You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
In function argument, applying a generic type that extends an object type to a conditional type operator doesn't specialise #56011
In unexpectedlyDoesntWork, input seems to be type inferred as Id<Vals> i.e. Vals, and so input.a is inferred as Vals. Hence the satisfies clause fails. This is not the most precise interpretation, since B is known to extend Family and hence be of object type with a member a, and so Id<B> should specialise to { [K in keyof B]: Id<B[K]> }.
🙂 Expected behavior
I expect input.a to be inferred as type Id<B["a"]> . This occurs in the worksFine function, where the only difference is that the function value has applied the non-conditional type operator IdObj to B.
It's a design limitation. There are numerous issues about this. Resolving of conditional types involving unbound generic type arguments are deferred until the type is known. In your example the type of B is not known within the function, so the conditional type is not resolved.
Thank you for the swift response! I see. And by "design limitation" you mean that there is no intention of trying to support this, even if it is theoretically possible?
I want to add that it seems to me that this case is more resolvable than #53455. In that issue, it's theoretically impossible to resolve the conditional (in isolation within the body of the function) because the unbound generic type U could either extend undefined or not.
Contrarily, in the example of this issue, the unbound generic type B extends Family and so must be an object type, which means that the conditional could be resolved.
In other words, and with naivety of the hairy details of the TypeScript type checker, I don't see why conditionals that can be resolved at type checking time necessarily need to be deferred.
There are actually cases where the compiler can resolve conditional types over generics if it can prove they always resolve a particular way. However, in your given example, the conditional type is distributive (i.e. it maps over unions) - which to my knowledge is always deferred because the compiler has no way of knowing how many times the conditional has to be applied. It might even be 0 times, if the input type happens to be never!
btw, your Playground and posted code differ. The issue form tells you not to do this
Sorry, I realized after posting the first link that I could simplify the example. After doing so, I forgot to refresh the link to the playground. Will do so now.
However, in your given example, the conditional type is distributive (i.e. it maps over unions) - which to my knowledge is always deferred because the compiler has no way of knowing how many times the conditional has to be applied. It might even be 0 times, if the input type happens to be never!
In my example the first application of the conditional may be statically determined: since B extends Family and Family is an object type, then B must be an object type. Except if B is never, but that seems like a special case that is anyway treated differently in the conditional (e.g. Id<never> is never despite this not being either of the return types of the conditional).
Id<never> is never because never is considered an empty union, so there are no types to evaluate the conditional against. It’s like .map() over an empty array.
Anyway, you know that B is an object type, but you don’t know whether it’s a union of object types - in which case Id<B> would also resolve to a union. TS must therefore defer it because it doesn’t know how many times it will have to apply the conditional. Now you might say here, well, the type you get will always be compatible, but assignability checks don’t work that way; there’s no higher-order machinery that can say “this will always resolve to a compatible type and is therefore assignable” without actually being able to resolve it. It’s just not part of the design of the compiler. Hence, Design Limitation.
there’s no higher-order machinery that can say “this will always resolve to a compatible type and is therefore assignable” without actually being able to resolve it. It’s just not part of the design of the compiler. Hence, Design Limitation.
Of course, I can't argue with this not knowing the design of the compiler. It's just still surprising to me that such a mechanism should be problematic to implement. It seems to me a matter of implementing a pattern match saying that a type internally represented as:
[T := Possible Union<B extends some object type>] extends object type ? trueCase<T> : falseCase<T>
may immediately be reduced internally to
PossibleUnion<trueCase<B extends some object type>
But anyway, issue closed, I'll just have to work around it, and please don't feel you need to respond to this.
Johan Rosenkilde (@johanrosenkilde) FWIW, your PossibleUnion abstraction illustrates the issue quite well - notice that still exists in the imagined reduced type. So the partially resolved type still necessarily has a quantifier and TS doesn't reason about that kind of thing in type checks. It just sees a deferred type. That's what I meant about the lack of higher-order machinery. I don't expect the compiler to be rearchitected to support that, but stranger things have happened when Anders Hejlsberg (@ahejlsberg) drops a PR out of nowhere so...1 😅
Footnotes
Nope, Design Limitation isn't necessarily a death sentence for an issue. It just means "we can't implement/fix this without adding new abstractions and possibly rethinking how adjacent features work." ↩
Activity
MartinJohns commentedon Oct 6, 2023
It's a design limitation. There are numerous issues about this. Resolving of conditional types involving unbound generic type arguments are deferred until the type is known. In your example the type of
Bis not known within the function, so the conditional type is not resolved.Duplicate of #53455 and others.
johanrosenkilde commentedon Oct 6, 2023
Thank you for the swift response! I see. And by "design limitation" you mean that there is no intention of trying to support this, even if it is theoretically possible?
I want to add that it seems to me that this case is more resolvable than #53455. In that issue, it's theoretically impossible to resolve the conditional (in isolation within the body of the function) because the unbound generic type
Ucould either extend undefined or not.Contrarily, in the example of this issue, the unbound generic type
BextendsFamilyand so must be an object type, which means that the conditional could be resolved.In other words, and with naivety of the hairy details of the TypeScript type checker, I don't see why conditionals that can be resolved at type checking time necessarily need to be deferred.
fatcerberus commentedon Oct 6, 2023
There are actually cases where the compiler can resolve conditional types over generics if it can prove they always resolve a particular way. However, in your given example, the conditional type is distributive (i.e. it maps over unions) - which to my knowledge is always deferred because the compiler has no way of knowing how many times the conditional has to be applied. It might even be 0 times, if the input type happens to be
never!fatcerberus commentedon Oct 6, 2023
btw, your Playground and posted code differ. The issue form tells you not to do this
johanrosenkilde commentedon Oct 6, 2023
Sorry, I realized after posting the first link that I could simplify the example. After doing so, I forgot to refresh the link to the playground. Will do so now.
johanrosenkilde commentedon Oct 6, 2023
In my example the first application of the conditional may be statically determined: since
B extends FamilyandFamilyis an object type, thenBmust be an object type. Except ifBisnever, but that seems like a special case that is anyway treated differently in the conditional (e.g.Id<never>isneverdespite this not being either of the return types of the conditional).fatcerberus commentedon Oct 6, 2023
Id<never>isneverbecauseneveris considered an empty union, so there are no types to evaluate the conditional against. It’s like.map()over an empty array.Anyway, you know that
Bis an object type, but you don’t know whether it’s a union of object types - in which caseId<B>would also resolve to a union. TS must therefore defer it because it doesn’t know how many times it will have to apply the conditional. Now you might say here, well, the type you get will always be compatible, but assignability checks don’t work that way; there’s no higher-order machinery that can say “this will always resolve to a compatible type and is therefore assignable” without actually being able to resolve it. It’s just not part of the design of the compiler. Hence, Design Limitation.typescript-bot commentedon Oct 9, 2023
This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
johanrosenkilde commentedon Oct 9, 2023
Thanks for explaining this, Bruce Pascoe (@fatcerberus)
Of course, I can't argue with this not knowing the design of the compiler. It's just still surprising to me that such a mechanism should be problematic to implement. It seems to me a matter of implementing a pattern match saying that a type internally represented as:
may immediately be reduced internally to
But anyway, issue closed, I'll just have to work around it, and please don't feel you need to respond to this.
fatcerberus commentedon Oct 9, 2023
Johan Rosenkilde (@johanrosenkilde) FWIW, your
PossibleUnionabstraction illustrates the issue quite well - notice that still exists in the imagined reduced type. So the partially resolved type still necessarily has a quantifier and TS doesn't reason about that kind of thing in type checks. It just sees a deferred type. That's what I meant about the lack of higher-order machinery. I don't expect the compiler to be rearchitected to support that, but stranger things have happened when Anders Hejlsberg (@ahejlsberg) drops a PR out of nowhere so...1 😅Footnotes
Nope,
Design Limitationisn't necessarily a death sentence for an issue. It just means "we can't implement/fix this without adding new abstractions and possibly rethinking how adjacent features work." ↩