2

I want to write a function f that accepts a single argument of type A, where A must be an array that contains an element of a certain type T. For simplicity, assume that T = 'ok' (singleton type of the string 'ok').

Using the idea from this answer, I obtained the following preliminary solution:

function f<
  E,
  A extends (ReadonlyArray<E> | [E]) & {[K in keyof A]: {[T in K]: 'ok'}}[number]
>(a: A) {}

As in the answer quoted above, it indeed works.

But I could not make any sense of the | [E]-part, so I decided to check whether I can remove it, or replace it by yet another type:

function f<
  E, 
  X, 
  A extends (ReadonlyArray<E> | [X]) & {[K in keyof A]: {[T in K]: 'ok'}}[number]
>(a: A) {}

This also works, i.e. it can differentiate between arrays with or without element 'ok':

f(['a', 'b', 'ok', 'z']) // compiles, good
f(['a', 'b', 'z'])       // does not compile, good

My problem is that I can't understand why it doesn't work if I remove the [X] part. It seems completely unrelated to anything what's going on in the code:

  • Arity of [X] does not match the arity of the tuples
  • The type X isn't actually tied to anything, it has no bounds containing either E or A or string or 'ok' or anything else.

What is the | [X] doing, exactly?

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93

1 Answers1

3

Let's take a simple example:

function foo<T extends number[]>(t: T): T { return t }
foo([1, 2, 3]) // number[]

will have an array return type inferred.

function foo2<T extends number[] | [number]>(t: T): T { return t }
foo2([1, 2, 3]) // [number, number, number]

| [number] forces TypeScript to infer a tuple instead of an array, without using as const (a hidden compiler rule, if you will).

You can even keep the number literals narrow by adding a type parameter R for the array items extending a primitive type:

function foo3<R extends number, T extends R[] | [R]>(t: T): T { return t }
foo3([1, 2, 3]) // [1, 2, 3]

Playground

bela53
  • 3,040
  • 12
  • 27
  • "a hidden compiler rule" - Does this hidden rule have any informal name, or is it mentioned in any kind of official documentation anywhere? – Andrey Tyukin Nov 18 '20 at 11:10
  • 1
    It is not part of the official TS handbook, but has been mentioned by language creator Anders Hejlsberg himself in a GitHub issue as valid workaround. So we can assume, it is here to stay in the compiler. I would need to search the concrete issue again, link not at hand currently. – bela53 Nov 18 '20 at 11:16
  • Did you mean this: https://github.com/microsoft/TypeScript/issues/27179#issuecomment-422911925 ? – Andrey Tyukin Nov 18 '20 at 11:25
  • 1
    Yes, indeed. Especially the one comment before: https://github.com/microsoft/TypeScript/issues/27179#issuecomment-422606990 – bela53 Nov 18 '20 at 11:29
  • Could you then link it more prominently in your answer, just so it's obvious that it's something that Hejlsberg himself approved of? (alternatively, I could edit the link into your answer, if you don't mind) – Andrey Tyukin Nov 18 '20 at 11:30