Skip to content

Unexpected any when using conditional type #59630

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
mstniy opened this issue Aug 14, 2024 · 12 comments
Closed

Unexpected any when using conditional type #59630

mstniy opened this issue Aug 14, 2024 · 12 comments
Labels
Not a Defect This behavior is one of several equally-correct options

Comments

@mstniy
Copy link

mstniy commented Aug 14, 2024

πŸ”Ž Search Terms

conditional type any array

πŸ•— Version & Regression Information

  • This changed between versions 4.9.5 and 5.0.2

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.0-beta#code/C4TwDgpgBAKhDOwoF4oDsCuBbARhATgNoC6UEAHsBGgCbxQCGaIJUA-OtnkcYZrgVIAudBABuBANxA

πŸ’» Code

type Test = number[] extends any[] ? number[][number] : never;

πŸ™ Actual behavior

Test evaluates to any

πŸ™‚ Expected behavior

Test evaluates to number

Additional information about the issue

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.

@jcalz
Copy link
Contributor

jcalz commented 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
Copy link
Author

mstniy commented 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
Copy link

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?

For me, I don't care the use case. It's a bug!

@RyanCavanaugh RyanCavanaugh added the Not a Defect This behavior is one of several equally-correct options label Aug 15, 2024
@RyanCavanaugh
Copy link
Member

For consistency, every time you have something like this

type V = { a: unknown };
type Type<T> = T extends V ? T['a'] : never;

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.

@ruojianll
Copy link

ruojianll commented Aug 16, 2024

@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.

type V = number // a given type
type Type<T> = T extends V ? T : never;// the true branch is V
type K = Type<string>// the true branch is T, also string.

@mstniy
Copy link
Author

mstniy commented Aug 16, 2024

For consistency, every time you have something like this

type V = { a: unknown };
type Type<T> = T extends V ? T['a'] : never;

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.

@RyanCavanaugh
Copy link
Member

Why is the true branch T&V, not T?

The example demonstrates why.

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.

@ruojianll
Copy link

ruojianll commented Aug 16, 2024

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.

type V = { a: unknown };
type Type<T> = T extends V ? 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.

type T = 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.

@RyanCavanaugh

@RyanCavanaugh
Copy link
Member

You can't just replace T with V, because then other stuff doesn't work. You wouldn't be allowed to write T['a'] here because 'a' isn't in V:

type V = { b: string };
type Foo<T extends { a: string }> = T extends V ? T['a'] : never;

@ruojianll
Copy link

Should it be { a: string } & V not T & V in generic type definition? Is it necessary to use T & V in the call type T = Type<{a:string}>?

@RyanCavanaugh
Copy link
Member

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.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Not a Defect" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Aug 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Not a Defect This behavior is one of several equally-correct options
Projects
None yet
Development

No branches or pull requests

5 participants