Why using never type as array indexer returns union of types of array values

Viewed 118
type T<A> = A[never];

const tuple = ['1', '2'] as const

const array = ['1', '2']

type UninType = T<typeof tuple> // "1" | "2"

type StringType = T<typeof array> // string

Why is it done this way, what's the point of this behavior

1 Answers

Short answer:

This behavior is just an unintended consequence of how indexed access types were implemented. It was brought up at some point that T[never] should be never no matter what T is; this was partially fixed; for T with no index signatures.

The fix for index signatures was put off because at the time it would have broken other things, and having T[never] resolve to the index signature type was accidentally preventing another problem from showing up. Nowadays it probably would be fine to implement the full fix, but I guess nobody ever got around to it, probably because it's not really a critical issue. You might want to open a new issue asking for this to be addressed, if you care a lot.


Long answer:

If you look at microsoft/TypeScript#11929, the pull request that implemented indexed access types, it describes the (then) process for determining the type T[K]:

  • If K is a type assignable to number and T contains a numeric index signature, T[K] is the type of that numeric index signature.
  • If K is a type assignable to string and T contains a string index signature, T[K] is the type of that string index signature.

In your case, since array and tuple types have a numeric index signature, and since the never type is assignable to every type, then indexing into an array or tuple type with never will yield the type of the numeric index signature.

That's consistent with your results anyway.


Now at some point people noticed that indexing into a type like {a: string} with never was resulting in any and erroring out. This was not desirable; it seems that you should be able to index with never and the result should also be never, like this:

type SomeObj = {a: string}[never]
// type SomeObj = never

So this was filed as an issue in microsoft/TypeScript#22042 and it was implemented in microsoft/TypeScript/pull/22787. (which is why the above SomeObj is never and not any).

In the comments on that fix, someone noted that:

this change only affects types with no index signature. So for example:

  • {[x: string]: boolean}[never] is boolean (unchanged behaviour)
  • {foo: boolean}[never] is never (new behaviour, previously was an error)

With that qualification, this is a non-breaking change because it only affects cases that are currently errors.

One of the TS team then replies that

After discussing it in person with @ahejlsberg, we didn't like the fact that ({ [n: number]: boolean})[never] is inconsistent: it's not never like everything else...

but they couldn't fix it. At the time, keyof ({[n: number]: boolean}) was never instead of number. I think we can all agree that if XXX is {[n: number]: boolean}, then we want XXX[keyof XXX] to be boolean. But since keyof XXX was never, then this sort of forced them to have XXX[never] also be boolean and not never. They couldn't fix it without breaking something that was accidentally working.

Since then, TypeScript 2.9 introduced a change to allow number and symbol types to come out of keyof, implemented in microsoft/TypeScript#23592. So now we have the right behavior:

type KeyofNumericIdxSig = keyof { [x: number]: boolean }
// type KeyofNumericIdxSig = number

But at the time microsoft/TypeScript#22787 was implemented, this had not been done. And so the language architect @ahejlsberg himself said:

[This PR] moves us closer to where we eventually want to be, i.e. that T[never] is always never regardless of T. Longer term, if and when we fix keyof { [x: number]: T } to produce numeric-string (or whatever we call it) instead of never, we can then also change { [x: number]: T }[never] to be never.

But it looks like they never came back and completed this.


So there you are. It's possible that if someone were to open a feature request issue linking to both microsoft/TypeScript#22787 and microsoft/TypeScript#23592 and pointing to this comment in particular, they would say "oh gee, yeah we never did that" and implement it. Or they might say "oh yeah we tried that and it did bad things elsewhere." Or even "we don't want to worry too much about this unless a lot more people complain about it". I can't really predict which.

Related