Skip to content
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

index never on tuple returns union #53345

Closed
unional opened this issue Mar 18, 2023 · 12 comments
Closed

index never on tuple returns union #53345

unional opened this issue Mar 18, 2023 · 12 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@unional
Copy link
Contributor

unional commented Mar 18, 2023

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$$
@Andarist
Copy link
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.

@unional
Copy link
Contributor Author

unional commented Mar 18, 2023

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.

@unional
Copy link
Contributor Author

unional commented Mar 19, 2023

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

@fatcerberus
Copy link

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.

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Mar 20, 2023
@RyanCavanaugh
Copy link
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.

@unional
Copy link
Contributor Author

unional commented Mar 21, 2023

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

@typescript-bot
Copy link
Collaborator

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

@unional
Copy link
Contributor Author

unional commented Mar 24, 2023

Reopen?

@RyanCavanaugh
Copy link
Member

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

@unional
Copy link
Contributor Author

unional commented Mar 24, 2023

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

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

@RyanCavanaugh
Copy link
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"

@unional
Copy link
Contributor Author

unional commented Mar 24, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

5 participants