A type cannot be narrowed on assignment. A predicate function is "more specific" than a function returning a boolean.
See these two types:
type SessionIsCart = (session: Session) => session is Cart;
type SessionTest = (session: Session) => boolean;
Every SessionIsCart is a SessionTest, but not the opposite.
For example:
function IsSessionExplicitlyNotACart(session: Session): boolean {
// Differentiate between {} and {items: undefined}
return 'items' in session && session.items === undefined;
}
That function also returns a boolean, but it is not a predicate that checks if the session is a Cart.
Your function has no signature, so it is inferred from the return that it returns a boolean. It is assignable to the SessionTest above, but not SessionIsCart. For the function to become a predicate, you must explicitly tell TS that the boolean returned by this function has a special meaning by writing the entire signature:
const sessionHasCart: SessionHasCart = (session: Session): session is Cart => {
return (session as Cart).cart !== undefined
}
Or the generic version
As @caTS mentioned in the comments, type predicates are a subset of the boolean type, much like symbol constants are a subset of the symbol type and string literals are a subset of the string type. But they are normal types.
Also, a type guard can be generic and take part in inference without any problems.
This is one example of using type guards for a functional filtering style. I use these exact functions in many places at work, along with other helpful functions that I made.