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
Unexpected any when using conditional typeΒ #59630
This seems to be the most minimal example that reproduces the issue.
It only happens when the type of the array being branched on is the same as the type of the array being indexed into in the first branch.
It does not happen when the condition is changed to number extends any
It does not happen when both of the number[]s are changed into a generic, which is set to number[].
It does not happen when the any[] is changed to unknown[].
Changing the first branch to true causes the expression to evaluate to true, so Typescript is indeed evaluating the first branch.
I agree this looks weird, but itβs hard for me to see the intent when writing a conditional type like this where neither the source nor the target is generic. Whatβs happening now is similar to treating number[] inside the true branch as if it were (number[] & any[]). Maybe it should do something more intelligent than that butβ¦ why write types like this? Whatβs the use case?
I agree this looks weird, but itβs hard for me to see the intent when writing a conditional type like this where neither the source nor the target is generic. Whatβs happening now is similar to treating number[] inside the true branch as if it were (number[] & any[]). Maybe it should do something more intelligent than that butβ¦ why write types like this? Whatβs the use case?
in the true branch, T always acts as T & V. This is critical to make types like the one shown work (otherwise you'd get the error "Type 'a' cannot be used to index type 'T'".
It's legitimate to do this even on non-type parameters! For example, you might have a library that needs to test the global type environment and light up certain features in post-ES2022 targets. You can also use it to induce weirdness on purpose, but that's a "succeeded at trying to fail" kind of situation.
Ryan Cavanaugh (@RyanCavanaugh) Why is the true branch T&V, not T? In my opinion, it should be V in generic type definition , and it should be T in generic type calls.
typeV=number// a given typetypeType<T>=TextendsV ? T : never;// the true branch is VtypeK=Type<string>// the true branch is T, also string.
in the true branch, T always acts as T & V. This is critical to make types like the one shown work (otherwise you'd get the error "Type 'a' cannot be used to index type 'T'".
It's legitimate to do this even on non-type parameters! For example, you might have a library that needs to test the global type environment and light up certain features in post-ES2022 targets. You can also use it to induce weirdness on purpose, but that's a "succeeded at trying to fail" kind of situation.
I see the point here. Is there a reason number[] & any[] does not simplify to number[]?
Same for number & any. I feel like it should just simplify to number, but it does not.
I see the point here. Is there a reason number[] & any[] does not simplify to number[]?
Because T & U means you can do anything you could have done with T or U. So if arr.push("blah") is legal on either operand, it should be legal for the intersection too.
Because T & U means you can do anything you could have done with T or U. So if arr.push("blah") is legal on either operand, it should be legal for the intersection too.
typeV={a: unknown};typeType<T>=TextendsV ? T['a'] : never;// in true branch, T is { a: any }
In the example, the true branch T is a subtype of V, so we could use a as it index. Consider T as V, T & V is no necessary.
typeT=Type<{a:string}>.// in true branch, T is { a: string }
In this call, the true branch T is {a:string}, T & V is not necessary too.
It should be V in generic type definition , and it should be T in generic type calls.
It can't be { a: string } because then an instantiation won't pick up a more-specific type, e.g. when you write Foo<{ a: "hello" }>.
I think I'm done explaining generics from first principles in this thread; please use the Discord or Stack Overflow if you have further questions about why this works the way it does.
Activity
jcalz commentedon Aug 14, 2024
Reminds me of #59430,
ultimately caused by #37348.
I agree this looks weird, but itβs hard for me to see the intent when writing a conditional type like this where neither the source nor the target is generic. Whatβs happening now is similar to treating
number[]inside the true branch as if it were(number[] & any[]). Maybe it should do something more intelligent than that butβ¦ why write types like this? Whatβs the use case?mstniy commentedon Aug 14, 2024
I was trying to reduce a rather complex type error in a real-world application. Chasing the weirdness led me here.
ruojianll commentedon Aug 15, 2024
For me, I don't care the use case. It's a bug!
RyanCavanaugh commentedon Aug 15, 2024
For consistency, every time you have something like this
in the true branch,
Talways acts asT & V. This is critical to make types like the one shown work (otherwise you'd get the error "Type 'a' cannot be used to index type 'T'".It's legitimate to do this even on non-type parameters! For example, you might have a library that needs to test the global type environment and light up certain features in post-ES2022 targets. You can also use it to induce weirdness on purpose, but that's a "succeeded at trying to fail" kind of situation.
ruojianll commentedon Aug 16, 2024
Ryan Cavanaugh (@RyanCavanaugh) Why is the true branch
T&V, notT? In my opinion, it should beVin generic type definition , and it should beTin generic type calls.mstniy commentedon Aug 16, 2024
I see the point here. Is there a reason
number[] & any[]does not simplify tonumber[]?Same for
number & any. I feel like it should just simplify tonumber, but it does not.RyanCavanaugh commentedon Aug 16, 2024
The example demonstrates why.
Because
T & Umeans you can do anything you could have done withTorU. So ifarr.push("blah")is legal on either operand, it should be legal for the intersection too.ruojianll commentedon Aug 16, 2024
In the example, the true branch
Tis a subtype ofV, so we could useaas it index. ConsiderTasV,T & Vis no necessary.In this call, the true branch T is
{a:string},T & Vis not necessary too.Ryan Cavanaugh (@RyanCavanaugh)
RyanCavanaugh commentedon Aug 16, 2024
You can't just replace
TwithV, because then other stuff doesn't work. You wouldn't be allowed to writeT['a']here because'a'isn't inV:ruojianll commentedon Aug 16, 2024
Should it be
{ a: string } & VnotT & Vin generic type definition? Is it necessary to useT & Vin the calltype T = Type<{a:string}>?RyanCavanaugh commentedon Aug 16, 2024
It can't be
{ a: string }because then an instantiation won't pick up a more-specific type, e.g. when you writeFoo<{ a: "hello" }>.I think I'm done explaining generics from first principles in this thread; please use the Discord or Stack Overflow if you have further questions about why this works the way it does.
typescript-bot commentedon Aug 19, 2024
This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.