Conditional types that depend on a generic type parameter T
can, in general, have arbitrarily complex behavior. Even something simple-looking like T extends AAA ? BBB : CCC
can have interesting results due to distribution over unions in T
. Currently the compiler just completely defers evaluation of such types until such time as the generic type arguments are specified... that is, when the type is no longer generic. Deferred generic conditional types are essentially opaque to the compiler; it can't really tell what values might or might not be assignable to them, so it will tend to complain if you try (e.g., return someValue
inside a function whose return type is a generic conditional type).
It would be nice if the compiler could eagerly resolve generic conditional types in cases where there is enough information about the generic type parameter to do so, but that's not what happens. If the question is "why", the answer is that it's just too difficult to do it in a way that works well enough to be useful and without degrading compiler performance too much.
This has been the subject of quite a few issues in GitHub, and inside these issues are the closest thing I've found to an official answer as to why it's like this:
microsoft/TypeScript#52144 "Resolve deferred conditional types to their true branch when instantiated with a type parameter constrained to the tested type". This is currently an open feature request. According to this comment
The reason it's deferred is that, while it would be safe to resolve to the true branch, for a generic it's not safe to resolve to the false branch because the type parameter might be instantiated with some more-specific type that actually does go to the true branch.
We don't yet have a concept of a "partial deferral"; it would be interesting to try. It'd be pretty tricky.
microsoft/TypeScript#48243 "Conditional type doesn't go to true or false branch.". This is closed as a Working as Intended. According to this comment:
Conditional types are not necessarily linear (meaning that they have predictable behavior between T
and an a subtype of T
), and figuring out whether or not they are requires reasoning in the form of "Does any type exist such that this type would behave in the other way?", which is not very tractable.
There are undoubtedly more, but hopefully that's enough to convey the reasoning behind the current behavior.
As for the other part of the question with indexed accesses in generics, the reason that stays deferred is because almost any type has subtypes, including number
(numeric literal types like 0.5
and 42
exist), so if T extends {foo: number}
, its foo
property might be narrower than number
, and resolving T["foo"]
to number
would destroy such information.