Skip to content

index never on tuple returns union #53345

Closed

Description

@unional

Bug Report

[1, 2][never] // 2 | 1

Is this a bug?

🔎 Search Terms

index, never, tuple

🕗 Version & Regression Information

TypeScript: 3.2 - 5.0.2

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about never

⏯ Playground Link

Playground link with relevant code

💻 Code

[1, 2][never] // 2 | 1

🙁 Actual behavior

[1, 2][never] // 2 | 1

🙂 Expected behavior

[1, 2][any] // 1 | 2

[1, 2][never] // never ----or Error: A tuple type cannot be indexed with `never`.----

// is the similar reasoning as:
[1, 2][2] // Tuple type '[1, 2]' of length '2' has no element at index '2'.
[1, 2][-1] // A tuple type cannot be indexed with a negative value.

UPDATE:

or Error: A tuple type cannot be indexed with never

is strike out, as the expression [1, 2][never] is value as:

$$keyof [1, 2] \in never$$

Activity

  1. Andarist commented on Mar 18, 2023

    @Andarist
    Contributor

    This happens for all index signatures, for example:

    type Rec = Record<string, boolean>[never] // boolean

    isApplicableIndexType here just ends up calling isTypeAssignableTo(source, target) and... well, never is assignable to everything.

  2. unional commented on Mar 18, 2023

    @unional
    ContributorAuthor

    Yes, never is assignable to everything:

    const x: number = 1 as never

    The question is what does Array<T>[never] mean.

    type X = ['a', 'b'][any] // 'a' | 'b'
    type Y = ['a', 'b'][number] // 'a' | 'b'
    type Z = ['a', 'b'][never] // 'a' | 'b'

    playground

    any and number is a superset of 0 and 1, so yielding 'a' | 'b' make sense as the category is mapped correctly. number is narrowed to 0 and 1 thus the result is the union of values.
    (UPDATE: or may be not? Maybe resolving to 'a' | 'b' | undefined make more sense? Or since any and number contains invalid index types such as negative number or non-number, it should be invalid?)

    But never supposed to be the bottom type of everything. From handbook:

    The never type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never (except never itself). Even any isn’t assignable to never.

    So to me yielding 'a' | 'b' doesn't sound right.

  3. unional commented on Mar 19, 2023

    @unional
    ContributorAuthor

    btw, while union order doesn't matter, [1, 2, 3][number] yields 2 | 1 | 3 instead of 1 | 2 | 3 is kind of weird.

    Playground yields 3 | 1 | 2 instead...
    Both Playground and local in my computer (Windows 7) are using TypeScript 5.0.2.

    playground

  4. fatcerberus commented on Mar 19, 2023

    @fatcerberus

    Union order is a pure implementation detail: It depends on the order the constituent types are first instantiated by the compiler and thus can change depending on the surrounding code, the order in which files are compiled (in a multi-file project) or even with different compiler settings. For all intents and purposes, you should treat it as random.

  5. RyanCavanaugh commented on Mar 20, 2023

    @RyanCavanaugh
    Member

    I don't follow the logic in the OP at all.

    • Indexing [1, 2][keyof [1, 2]] is legal
    • never is a legal keyof [1, 2]
    • Therefore [1, 2][never] is legal too

    I think you could argue either way whether the "correct" bound on that type is [1, 2] or never, but plainly both are sound. Absent a more concrete argument for changing it, I don't see what the goal is here.

  6. unional commented on Mar 21, 2023

    @unional
    ContributorAuthor

    Yes, never is a legal value of keyof [1, 2] = (0 | 1) as never is considered as the "empty union" ().

    And yes, the question should focus on what it resolves/bounds to.

    The problem and confusion is that the current behavior does not map logically.

    Consider type as set:

    $$never \in 0 \in (0 | 1) \in any$$

    When mapped using "functor" [1, 2][N], the result is:

    [1, 2][never] // 1 | 2
    [1, 2][0] // 1
    [1, 2][0 | 1] // 1 | 2
    [1, 2][any] // 1 | 2

    That means:

    $$never \in 0 \in (0 | 1) \in any$$

    is transformed to

    $$(1 | 2) \notin 1 \in (1 | 2) \in (1 | 2)$$

    meaning never, which is a bottom type of all, is mapped to a larger set then its superset.

    That's why [1, 2][never] // 1 | 2 should be incorrect.

    It should resolve to never

  7. typescript-bot commented on Mar 24, 2023

    @typescript-bot
    Contributor

    This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  8. unional commented on Mar 24, 2023

    @unional
    ContributorAuthor

    Reopen?

  9. RyanCavanaugh commented on Mar 24, 2023

    @RyanCavanaugh
    Member

    I'd consider it, but again, a motivating scenario is needed given that the current result is still a correct upper bound.

  10. unional commented on Mar 24, 2023

    @unional
    ContributorAuthor

    Do you think my reasoning here is correct? #53345 (comment)

    That tries to reason that returning 1 | 2 is the incorrect behavior.

  11. RyanCavanaugh commented on Mar 24, 2023

    @RyanCavanaugh
    Member

    I think your logic and my logic are both correct, and both create correct upper bounds.

    Generally we've found it's disruptive to just go change stuff without being able to point to a code example and say "We made this work now" as justification for making those changes. Inevitably, changes cause breaks, and people want to know why you broke them, and there should be a better answer than "Someone used ∈ correctly"

  12. unional commented on Mar 24, 2023

    @unional
    ContributorAuthor

    No problem. I understand that changes could break someone else's code.

    I understand that TypeScript need to make trade-offs, balance between usability vs soundness.

    It might even be too late to change now, but things like these make the type system harder to reason, thus make it harder to understand and write correct (type-level) code.

    I'm going to have this logic in the At<A, N> type in type-plus. i.e.:

    type R = At<[1, 2, 3], never> // never

    This way, there is at least some way to obtain that behavior.

  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

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions