Search code examples
typescripttypestuplesmapped-types

TypeScript: Meaning of `| [T]`-constraint for compile-time checks of element membership in arrays


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?


Solution

  • 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