Skip to content

Unexpected any when using conditional typeΒ #59630

Closed as not planned
Closed as not planned

Description

πŸ”Ž 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.

Activity

  1. jcalz commented on Aug 14, 2024

    @jcalz
    Contributor

    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?

  2. mstniy commented on Aug 14, 2024

    @mstniy
    Author

    I was trying to reduce a rather complex type error in a real-world application. Chasing the weirdness led me here.

  3. ruojianll commented on Aug 15, 2024

    @ruojianll

    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!

  4. RyanCavanaugh commented on Aug 15, 2024

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

  5. ruojianll commented on Aug 16, 2024

    @ruojianll

    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.

    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.
  6. mstniy commented on Aug 16, 2024

    @mstniy
    Author

    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.

  7. RyanCavanaugh commented on Aug 16, 2024

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

  8. ruojianll commented on Aug 16, 2024

    @ruojianll

    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.

    Ryan Cavanaugh (@RyanCavanaugh)

  9. RyanCavanaugh commented on Aug 16, 2024

    @RyanCavanaugh
    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;
  10. ruojianll commented on Aug 16, 2024

    @ruojianll

    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}>?

  11. RyanCavanaugh commented on Aug 16, 2024

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

  12. typescript-bot commented on Aug 19, 2024

    @typescript-bot
    Contributor

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

  13. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Not a DefectThis behavior is one of several equally-correct options

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions