HomepageExploring TypeScript (TS 5.8 Edition)
You can support this book: buy it or donate
(Ad, please don’t block.)

37 Computing with tuple types

JavaScript’s Arrays are so flexible that TypeScript provides two different kinds of types for handling them:

In this chapter, we look at the latter – especially how to compute with tuples at the type level.

37.1 The syntax of tuple types

37.1.1 Basic syntax

Tuple types have this syntax:

[ Required, Optional?, ...RestElement[] ]

Examples:

type T = [string, boolean?, ...number[]];
const v1: T = ['a', true, 1, 2, 3];
const v2: T = ['a', true];
const v3: T = ['a'];
// @ts-expect-error: Type '[]' is not assignable to type 'T'.
const v4: T = [];

There is one additional rule: required elements can appear after a rest element – but only if there is no optional element before them:

type T1 = [number, ...boolean[], string]; // OK
type T2 = [...boolean[], string]; // OK

// @ts-expect-error: A required element cannot follow
// an optional element.
type T3 = [number?, ...boolean[], string];
37.1.1.1 Optional elements can only be omitted at the end
type T = [string, boolean?, ...number[]];

const v1: T = ['a', false, 1, 2, 3]; // OK
const v2: T = ['a']; // OK

// @ts-expect-error: Type 'number' is not assignable to
// type 'boolean'.
const v3: T = ['a', 1, 2, 3];

If the compiler option exactOptionalPropertyTypes is active, we can’t even do the following:

// @ts-expect-error: Type '[string, undefined, number, number, number]' is
// not assignable to type 'T'. Type at position 1 in source is not
// compatible with type at position 1 in target. Type 'undefined' is not
// assignable to type 'boolean'.
const v4: T = ['a', undefined, 1, 2, 3];

Note that this is similar to how JavaScript handles parameters and destructuring – e.g.:

function f(x, y=3, ...z) {
  return {x,y,z};
}

If we want to enable omitting elements in the middle, we can use a union:

// The `boolean` element can be omitted:
type T =
  | [string, boolean, ...number[]]
  | [string, ...number[]]
;
const v1: T = ['a', false, 1, 2, 3]; // OK
const v2: T = ['a', 1, 2, 3]; // OK

If there is a second parameter, it is assigned to y and does not become an element of z.

37.1.2 Variadic tuple elements

Variadic means “has variable (not fixed) arity”. The arity of a tuple is its length.

Variadic elements (or spread elements) enable spreading into tuples at the type level:

type Tuple1 = ['a', 'b'];
type Tuple2 = [1, 2];
type _ = Assert<Equal<
  [true, ...Tuple1, ...Tuple2, false], // type expression
  [ true, 'a', 'b', 1, 2, false ] // result
>>;

Compare that to spreading in JavaScript:

const tuple1 = ['a', 'b'];
const tuple2 = [1, 2];
assert.deepEqual(
  [true, ...tuple1, ...tuple2, false], // expression
  [ true, 'a', 'b', 1, 2, false ] // result
);

The type that is spread is usually a type variable and must be assignable to readonly any[] – i.e., it must be an Array or a tuple. It can have any length – hence the term “variadic”. The pull request “Variadic tuple types” describes spreading like this:

Intuitively, a variadic element ...T is a placeholder that is replaced with one or more elements through generic type instantiation.

37.1.2.1 Normalization of instantiated generic tuple types

The result of spreading is adjusted so that it always fits the shape described at the beginning of this section. To explore how that works, we’ll use the utility types Spread1 and Spread2:

type Spread1<T extends unknown[]> = [...T];
type Spread2<T1 extends unknown[], T2 extends unknown[]> =
  [...T1, ...T2]
;

type _ = [
  // A tuple with only a spread Array becomes an Array:
  Assert<Equal<
    Spread1<Array<string>>,
    string[]
  >>,
  
  // If an Array is spread at the end, it becomes a rest element:
  Assert<Equal<
    Spread2<['a', 'b'], Array<number>>,
    ['a', 'b', ...number[]]
  >>,
  
  // If two Arrays are spread, they are merged so that there
  // is at most one rest element:
  Assert<Equal<
    Spread2<Array<string>, Array<number>>,
    [...(string | number)[]]
  >>,
  
  // Optional elements after an Array are merged into it:
  Assert<Equal<
    Spread2<Array<string>, [number?, boolean?]>,
    (string | number | boolean | undefined)[]
  >>,
  
  // Optional elements `T` before required ones become `undefined|T`:
  Assert<Equal<
    Spread2<[string?], [number]>,
    [string | undefined, number]
  >>,
  
  // Required elements between Arrays are also merged:
  Assert<Equal<
    Spread2<[boolean, ...number[]], [string, ...bigint[]]>,
    [boolean, ...(string | number | bigint)[]]
  >>,
];

Note that we can only spread a type T if it is constrained via extends to an Array type:

type Spread1a<T extends unknown[]> = [...T]; // OK
// @ts-expect-error: A rest element type must be an array type.
type Spread1b<T> = [...T];

37.1.3 Labeled tuple elements

We can also specify labels for tuple elements:

type Interval = [start: number, end: number];

If one element is labeled, all elements must be labeled. For optional elements, the syntax changes with labels – the question mark (?) is added to the label, not the type (TypeScript will tell you during editing if you do it wrong):

type Tuple1 = [string, boolean?, ...number[]];
type Tuple2 = [requ: string, opt?: boolean, ...rest: number[]];

What do labels do? Not much: They help with autocompletion and are preserved by some type operations but have no other effect in the type system:

Therefore: If names matter, you should use an object type.

37.1.3.1 Extracted function parameters are labeled

If we extract function parameters, we get labeled tuple elements:

type _1 = Assert<Equal<
  Parameters<(sym: symbol, bool: boolean) => void>,
  [sym: symbol, bool: boolean]
>>;

Note that there is no way to check what the actual tuple element labels are – these checks succeed too:

// Different labels
type _2 = Assert<Equal<
  Parameters<(sym: symbol, bool: boolean) => void>,
  [HELLO: symbol, EVERYONE: boolean]
>>;

// No labels
type _3 = Assert<Equal<
  Parameters<(sym: symbol, bool: boolean) => void>,
  [symbol, boolean]
>>;
37.1.3.2 Use case: overloading

TypeScript uses labels as function parameters if a rest parameter has a tuple type:

function f1(...args: [str: string, num: number]) {}
  // function f1(str: string, num: number): void
function f2(...args: [string, number]) {}
  // function f2(args_0: string, args_1: number): void

Thanks to labels, tuples become better as an alternative to overloading because autocompletion can show parameter names:

// Overloading with tuples
function f(
  ...args:
    | [str: string, num: number]
    | [num: number]
    | [bool: boolean]
): void {
  // ···
}
// Traditional overloading
function f(str: string, num: number): void;
function f(num: number): void;
function f(bool: boolean): void;
function f(arg0: string | number | boolean, num?: number): void {
  // ···
}

The caveat is that the tuples can’t influence the return type.

37.1.3.3 Use case: preserving argument names when transforming functions

How that works is demonstrated when we handle partial application later in this chapter.

37.2 Types for tuples

37.2.1 Tuples and --noUncheckedIndexedAccess

If we switch on the tsconfig.json option noUncheckedIndexedAccess then TypeScript is more honest about what it knows about an indexable type.

With an Array, TypeScript never knows at compile time at which indices there are elements – which is why undefined is always a possible result with indexed reading:

const arr: Array<string> = ['a', 'b', 'c'];
const arrayElement = arr[1];
assertType<string | undefined>(arrayElement);

With a tuple, TypeScript knows the whole shape and can provide better types for indexed reading:

const tuple: [string, string, string] = ['a', 'b', 'c'];
const tupleElement = tuple[1];
assertType<string>(tupleElement);

37.2.2 Forcing Array literals to be inferred as tuples

By default, a JavaScript Array literal has an Array type:

// Array
const value1 = ['a', 1];
assertType<
  (string | number)[]
>(value1);

The most common way of changing that is via an as const annotation:

// Tuple
const value2 = ['a', 1] as const;
assertType<
  readonly ['a', 1]
>(value2);

But we can also use satisfies:

// Non-empty tuple
const value3 = ['a', 1] satisfies [unknown, ...unknown[]];
assertType<
  [string, number]
>(value3);

// Tuple (possibly empty)
const value4 = ['a', 1] satisfies [unknown?, ...unknown[]];
assertType<
  [string, number]
>(value4);

Note that as const also narrows the element types to 'a' and 1. With satisfies, they are string and number – unless we use as const for the elements:

// Tuple
const value5 = [
  'a' as const, 1 as const
] satisfies [unknown?, ...unknown[]];
assertType<
  ['a', 1]
>(value5);

If we omit the tuple element before the rest element (at the end), we are back to an Array type:

// Array
const value6 = ['a', 1] satisfies [...unknown[]];
assertType<
  (string | number)[]
>(value6);

There is one other type we can use for tuples:

// Tuple
const value7 = ['a', 1] satisfies unknown[] | [];
assertType<
  [string, number]
>(value7);

37.2.3 Using readonly to accept const tuples

If a type T is constrained to a normal array type then it doesn’t match the type of an as const literal:

type Tuple<T extends Array<unknown>> = T;
const arr = ['a', 'b'] as const;
// @ts-expect-error: Type 'readonly ["a", "b"]' does not satisfy
// the constraint 'unknown[]'.
type _ = Tuple<typeof arr>;

We can change that by switching to a ReadonlyArray:

type Tuple<T extends ReadonlyArray<unknown>> = T;
const arr = ['a', 'b'] as const;
type Result = Tuple<typeof arr>;
type _ = Assert<Equal<
  Result, readonly ['a', 'b']
>>;

The following two notations are equivalent:

ReadonlyArray<unknown>
readonly unknown[]

In this chapter, I don’t always make array types readonly because it adds visual clutter.

37.2.4 Enforcing a fixed Array length

We can use the following trick to enforce a fixed length for Array literals:

function join3<T extends string[] & {length: 3}>(...strs: T) {
  return strs.join('');
}
join3('a', 'b', 'c'); // OK

// @ts-expect-error: Argument of type '["a", "b"]' is not assignable
// to parameter of type 'string[] & { length: 3; }'.
join3('a', 'b');

The caveat is that this technique does not work if the strs come from a variable whose type is an Array:

const arr = ['a', 'b', 'c'];
// @ts-expect-error: Argument of type 'string[]' is not assignable
// to parameter of type 'string[] & { length: 3; }'.
join3(...arr);

In contrast, a tuple works:

const tuple = ['a', 'b', 'c'] as const;
join3(...tuple);

37.3 The keys of tuple types

The keys of an Array type look like this (note the Includes in the first line):

type _ = Assert<Includes<
  keyof Array<string>,
  number | 'length' | 'push' | 'join' // ...
>>;

We can see keys for Array indices (number), .length and Array methods.

The keys of a tuple type are similar, but, in addition to the broad type number for indices, they also have one stringified number for each index:

type _ = Assert<Includes<
  keyof ['a', 'b'],
  number | '0' | '1' | 'length' | 'push' | 'join'  // ...
>>;

Why are string literal types used and not number literal types? The latter disappear in a union with number:

type _ = Assert<Includes<
  number | 0 | 1,
  number
>>;

Note that the ECMAScript specification also uses string keys for Array elements (more information):

> Object.keys(['a', 'b'])
[ '0', '1' ]

37.3.1 Extracting the index keys (strings) of a tuple

This utility type returns all string keys of a tuple T that are indices:

type TupleIndexKeys<T extends ReadonlyArray<unknown>> =
  (keyof T) & `${number}`
;
type _ = Assert<Equal<
  TupleIndexKeys<['a', 'b']>,
  '0' | '1'
>>;

We use & to create the intersection type between the keys of T and the template literal type `${number}` – which is the type of all strings that are stringified numbers (see “Interpolating primitive types into template literals” (§38.2.5)).

37.3.2 Extracting the indices (numbers) of a tuple

Getting a tuple’s numeric indices (numbers, not stringified numbers) is more work:

type TupleIndices<T extends ReadonlyArray<unknown>> =
  StrToNum<keyof T>
;
type _ = Assert<Equal<
  TupleIndices<['a', 'b']>,
  0 | 1
>>;

TupleIndices uses the following helper type, which extracts string literal types with numbers and converts them to numbers.

type StrToNum<T> =
  T extends `${infer N extends number}` ? N : never // (A)
;
type _ = Assert<Equal<
  StrToNum<number | '0' | '1' | 'length' | 'push' | 'join'>,
  0 | 1
>>;

In line A, StrToNum uses a template literal type plus infer to parse a number inside a string literal type. If there is no number, it returns never. Since the conditional type in line A is distributive, we can use it to filter a union type (as shown at the end).

37.4 Mapping tuples via mapped types

A mapped type has the following syntax:

type MapOverType<Type> = {
  [Key in keyof Type]: Promise<Type[Key]>
};

37.4.1 How a mapped type handles the keys of a tuple type

Recall that keyof, applied to a tuple, produces a variety of values: method names, stringified indices, etc.

In its basic form, a mapped type helps us with tuples in two ways:

The following example demonstrates both phenomena. KeyToKey<T> returns a tuple whose elements are the string index keys of the tuple T:

type KeyToKey<T> = {
  [K in keyof T]: K
};
type _ = Assert<Equal<
  KeyToKey<['a', 'b']>,
  // Result is a tuple
  ['0', '1']
>>;

37.4.2 Mapping preserves the labels of tuple elements

Mapping preserves the labels of tuple elements:

type WrapValues<T> = {
  [Key in keyof T]: Promise<T[Key]>
};
type _ = Assert<Equal<
  WrapValues<[a: number, b: number]>,
  [a: Promise<number>, b: Promise<number>]
>>;

37.4.3 Tuples and mapped types with key remapping (as)

If we use key remapping (as) in a mapped type over a tuple then the result won’t be a tuple anymore and all keys of a tuple will be considered (vs. only its indices):

type KeyAsKeyToKey<T> = {
  [K in keyof T as K]: K
};
type _ = Assert<Equal<
  // Use Pick<> because result of KeyAsKeyToKey<> is large
  Pick<
    KeyAsKeyToKey<['a', 'b']>,
    '0' | '1' | 'length' | 'push' | 'join'
  >,
  // Result is an object, not a tuple
  {
    length: 'length';
    push: 'push';
    join: 'join';
    0: '0';
    1: '1';
  }
>>;

If we want to stick with tuple indices, we have to filter the result of keyof. To do that, we can use the utility types TupleIndexKeys that we have defined previously:

type StringTupleToObject<T extends ReadonlyArray<string>> = {
  [K in TupleIndexKeys<T> as T[K]]: K
};
type _ = Assert<Equal<
  StringTupleToObject<['a', 'b']>,
  {
    a: '0',
    b: '1',
  }
>>;

Note that TupleIndices returns string literal types – which explains the property values. If we prefer number literal types, we can use the previously defined utility type TupleIndices:

type StringTupleToObject<T extends ReadonlyArray<string>> = {
  [K in TupleIndices<T> as T[K]]: K
};
type _ = Assert<Equal<
  StringTupleToObject<['a', 'b']>,
  {
    a: 0,
    b: 1,
  }
>>;

37.4.4 Example: typing Promise.all()

This is what the type for Promise.all() looks like (I edited the actual code slightly):

We will use the following helper type which unwraps the Promises in a tuple:

type AwaitedTuple<T extends ReadonlyArray<unknown>> = {
  -readonly [K in keyof T]: Awaited<T[K]> // (A)
}
type _ = Assert<Equal<
  AwaitedTuple<readonly [Promise<number>, Promise<string>]>,
  [number, string]
>>;

Notes:

With that helper type, our version of Promise.all() is easy to type:

function promiseAll<
  T extends ReadonlyArray<unknown> | [] // (A)
>(values: T): Promise<AwaitedTuple<T>> {
  // ···
}
const result = promiseAll(
  [Promise.resolve(123), Promise.resolve('abc')]
);
assertType<Promise<[number, string]>>(result);

The constraint after extends in line A achieves two things:

37.5 Extracting union types from tuples

37.5.1 Applying the indexed access operator T[K] to a tuple

If we apply the indexed access operator T[K] to a tuple, we get the tuple elements as a union:

type UnionOf<Tup extends ReadonlyArray<unknown>> = Tup[number];

const flowers = ['rose', 'sunflower', 'lavender'] as const;
type _ = Assert<Equal<
  UnionOf<typeof flowers>,
  'rose' | 'sunflower' | 'lavender'
>>;

37.5.2 Extracting a union from a tuple of tuples

Sometimes, it makes sense to encode data as a collection of tuples – e.g. when we want to look up a tuple by any of its elements and performance is not as important. In contrast, Maps only support lookup by key well.

For Maps, it’s easy to compute the keys and the values – which we can use to constrain values when looking up data. Can we do the same for a tuple of tuples? We can, if we use the indexed access operator T[K] twice:

const englishSpanishGerman = [
  ['yes', 'sí', 'ja'],
  ['no', 'no', 'nein'],
  ['maybe', 'tal vez', 'vielleicht'],
] as const;

type English = (typeof englishSpanishGerman)[number][0];
type _1 = Assert<Equal<
  English, 'yes' | 'no' | 'maybe'
>>;

type Spanish = (typeof englishSpanishGerman)[number][1];
type _2 = Assert<Equal<
  Spanish, 'sí' | 'no' | 'tal vez'
>>;

37.5.3 Extracting a union from a tuple of objects

The same approach works for a tuple of objects:

const listCounterStyles = [
  { name: 'upperRoman', regExp: /^[IVXLCDM]+$/ },
  { name: 'lowerRoman', regExp: /^[ivxlcdm]+$/ },
  { name: 'upperLatin', regExp: /^[A-Z]$/ },
  { name: 'lowerLatin', regExp: /^[a-z]$/ },
  { name: 'decimal',    regExp: /^[0-9]+$/ },
] as const satisfies Array<{regExp: RegExp, name: string}>;

type CounterNames = (typeof listCounterStyles)[number]['name'];
type _ = Assert<Equal<
  CounterNames,
  | 'upperRoman' | 'lowerRoman'
  | 'upperLatin' | 'lowerLatin'
  | 'decimal'
>>;

37.6 Computing with tuple types

37.6.1 Extracting parts of tuples

To extract parts of tuples, we use infer.

37.6.1.1 Extracting the first element of a tuple

We infer the first element and ignore all other elements by using unknown as a wildcard type that matches anything.

type First<T extends Array<unknown>> =
T extends [infer F, ...unknown[]]
  ? F
  : never
;
type _ = Assert<Equal<
  First<['a', 'b', 'c']>,
  'a'
>>;
37.6.1.2 Extracting the last element of a tuple

The approach we used to extract the first element (in the previous example) also works for extracting the last element:

type Last<T extends Array<unknown>> =
T extends [...unknown[], infer L]
  ? L
  : never
;
type _ = Assert<Equal<
  Last<['a', 'b', 'c']>,
  'c'
>>;
37.6.1.3 Extracting the rest of a tuple (elements after the first one)

To extract the rest of a tuple (the elements after the first one), we make use the wildcard type unknown for the first element and infer what is spread after it:

type Rest<T extends Array<unknown>> =
T extends [unknown, ...infer R]
  ? R
  : never
;
type _ = Assert<Equal<
  Rest<['a', 'b', 'c']>,
  ['b', 'c']
>>;

37.6.2 Using a tuple of pairs as a lookup table

For many purposes, object literal types are very convenient as lookup tables: At the type level, lookup only works if keys are strings, numbers or symbols. Additionally, TypeScript doesn’t distinguish between strings and numbers. That mirrors how JavaScript works and prevents us from distinguishing between the number 1 and the string '1':

type LookupTable = {
  [1]: 'a',
};
type _ = [
  Assert<Equal<
    LookupTable[1], 'a'
  >>,
  Assert<Equal<
    LookupTable['1'], 'a'
  >>,
];

As an alternative, we can use a tuple of pairs (tuples with two elements) as a lookup table:

type LookupTable = [
  [undefined, 'undefined'],
  [null, 'null'],
  [boolean, 'boolean'],
  [number, 'number'],
  [bigint, 'bigint'],
  [string, 'string'],
];
type R = Assert<Equal<
  Lookup<LookupTable, string>, 'string'
>>;

These are the types that implement the lookup functionality:

type LookupOne<Pair extends readonly [unknown, unknown], Key> =
  Pair extends [Key, infer Value] ? Value : never;
type Lookup<Table extends ReadonlyArray<readonly [unknown, unknown]>, Key> =
  LookupOne<Table[number], Key>;

How does that work? Step 1: Go from a tuple of pairs to a union of pairs via an indexed access type (T[K]).

type _1 = Assert<Equal<
  LookupTable[number],
  | [undefined, 'undefined']
  | [null, 'null']
  | [boolean, 'boolean']
  | [number, 'number']
  | [bigint, 'bigint']
  | [string, 'string']
>>;

Step 2: Apply LookupOne to each of the pairs. That happens automatically if we apply that generic type to the union because its conditional type is distributive:

type _2 = [
  Assert<Equal<
    LookupOne<[undefined, 'undefined'], string>,
    never
  >>,
  Assert<Equal<
    LookupOne<[string, 'string'], string>,
    'string'
  >>,
];

Step 3: Since never is the empty set, we get the final result 'string' after the intermediate result of the distributed application of LookupOne is evaluated:

type _3 = Assert<Equal<
  never | never | never | never | never | 'string',
  'string' // final result
>>;

37.6.3 Concatenating tuples

To concatenate two tuples T1 and T2, we spread them both:

type Concat<T1 extends Array<unknown>, T2 extends Array<unknown>> =
  [...T1, ...T2]
;
type _ = Assert<Equal<
  Concat<['a', 'b'], ['c', 'd']>,
  ['a', 'b', 'c', 'd']
>>;

37.6.4 Recursion over tuples

To explore recursion over tuples, let’s implement wrapping tuple elements with recursion (where we previously used a mapped type):

Recursing over tuples in TypeScript

type WrapValues<Tup> =
  Tup extends [infer First, ...infer Rest] // (A)
    ? [Promise<First>, ...WrapValues<Rest>] // (B)
    : [] // (C)
;
type _ = Assert<Equal<
  WrapValues<['a', 'b', 'c']>,
  [Promise<'a'>, Promise<'b'>, Promise<'c'>]
>>;

We use a technique that is inspired by how functional programming languages recurse over lists:

In functional programming, First is often called Head and Rest is often called Tail.

37.6.4.1 Flattening a tuple of tuples

Let’s use recursion to flatten a tuple of tuples:

type Flatten<Tups extends Array<Array<unknown>>> =
  Tups extends [
    infer Tup extends Array<unknown>, // (A)
    ...infer Rest extends Array<Array<unknown>> // (B)
  ]
    ? [...Tup, ...Flatten<Rest>]
    : []
;
type _ = Assert<Equal<
  Flatten<[['a', 'b'], ['c', 'd'], ['e']]>,
  ['a', 'b', 'c', 'd', 'e']
>>;

In this case, the inferred types Tup and Rest are more complex – which is why TypeScript complains if we don’t use extends (line A, line B) to constrain them.

37.6.4.2 Filtering a tuple

The following code uses recursion to filter out empty strings in a tuple:

type RemoveEmptyStrings<T extends Array<string>> =
  T extends [
    infer First extends string,
    ...infer Rest extends Array<string>
  ]
    ? First extends ''
      ? RemoveEmptyStrings<Rest>
      : [First, ...RemoveEmptyStrings<Rest>]
    : []
;
type _ = Assert<Equal<
  RemoveEmptyStrings<['', 'a', '', 'b', '']>,
  ['a', 'b']
>>;

Note that we have to use recursion for filtering. There are two reasons why using a mapped type and key remapping via as won’t work:

type RemoveEmptyStrings<T extends Array<string>> = {
  [K in keyof T as (T[K] extends '' ? never : K)]: T[K]
};
type Filtered = RemoveEmptyStrings<['', 'a', '', 'b', '']>
  // type Filtered = {
  //   [x: number]: "" | "a" | "b";
  //   1: "a";
  //   3: "b";
  //   length: 5;
  //   toString: () => string;
  //   ...
  // }
37.6.4.3 Creating a tuple with a given length

If we want to create a tuple that has a given length Len, we are faced with a challenge: How do we know when to stop? We can’t decrement Len, we can only check if it is equal to a given value (line A):

type Repeat<
  Len extends number, Value,
  Acc extends Array<unknown> = []
> = 
  Acc['length'] extends Len // (A)
    ? Acc // (B)
    : Repeat<Len, Value, [...Acc, Value]> // (C)
;

type _ = [
  Assert<Equal<
    Repeat<3, '*'>,
    ['*', '*', '*']
  >>,
  Assert<Equal<
    Repeat<3, string>,
    [string, string, string]
  >>,
  Assert<Equal<
    Repeat<3, unknown>,
    [unknown, unknown, unknown]
  >>,
];

How does this code work? We use another functional programming technique and introduce an internal accumulator parameter Acc:

37.6.4.4 Computing a range of numbers

We can use the same technique to compute a range of numbers. Only this time, we append the current length of the accumulator to the accumulator:

type NumRange<Upper extends number, Acc extends number[] = []> =
  Upper extends Acc['length']
    ? Acc
    : NumRange<Upper, [...Acc, Acc['length']]>
;
type _ = Assert<Equal<
  NumRange<3>,
  [0, 1, 2]
>>;
37.6.4.5 Dropping initial elements

This is one way of implementing a utility type that removes the first Num elements of a Tuple:

type Drop<
  Tuple extends Array<unknown>,
  Num extends number,
  Counter extends Array<boolean> = []
> =
  Counter['length'] extends Num
    ? Tuple
    : Tuple extends [unknown, ...infer Rest extends Array<unknown>]
      ? Drop<Rest, Num, [true, ...Counter]>
      : Tuple
;
type _ = Assert<Equal<
  Drop<['a', 'b', 'c'], 2>,
  ['c']
>>;

This time, we use the accumulator variable Counter to count up – until Counter['length'] is equal to Num.

We can also use inference (idea by Heribert Schütz):

type Drop<
  Tuple extends Array<unknown>,
  Num extends number
> =
  Tuple extends [...Repeat<Num, unknown>, ...infer Rest]
    ? Rest
    : never
;

We use the utility type Repeat to compute a tuple where each element is the wildcard type unknown that matches any type. Then we match Tuple against a tuple pattern that begins with those elements. The remaining elements are the result we are looking for and we extract it via infer.

37.7 Real-world examples

37.7.1 Partial application that preserves parameter names

Let’s implement the function applyPartial(func, args) for partially applying a function func. It works similarly to the function method .bind():

function applyPartial<
  Func extends (...args: any[]) => any,
  InitialArgs extends unknown[],
>(func: Func, ...initialArgs: InitialArgs) {
  return (...remainingArgs: RemainingArgs<Func, InitialArgs>)
  : ReturnType<Func> => {
    return func(...initialArgs, ...remainingArgs);
  };
}

//----- Test -----

function add(x: number, y: number): number {
  return x + y;
}
const add3 = applyPartial(add, 3);
type _1 = Assert<Equal<
  typeof add3,
  // The parameter name is preserved!
  (y: number) => number
>>;

We return a partially applied func. To compute the type for the parameter remainingArgs, we remove the InitialArgs from the arguments of Func – via the following utility type:

type RemainingArgs<
  Func extends (...args: any[]) => any,
  InitialArgs extends unknown[],
> =
  Func extends (
    ...args: [...InitialArgs,
    ...infer TrailingArgs]
  ) => unknown
    ? TrailingArgs
    : never
;

//----- Test -----

type _2 = Assert<Equal<
  RemainingArgs<typeof add, [number]>,
  [y: number]
>>;

37.7.2 Typing a function zip()

Consider a zip() function that converts a tuple of iterables to an iterable of tuples (source code of an implementation):

> zip([[1, 2, 3], ['a', 'b', 'c']])
[ [1, 'a'], [2, 'b'], [3, 'c'] ]

The following utility type Zip computes a return type for it:

type Zip<Tuple extends Array<Iterable<unknown>>> =
  Iterable<
    { [Key in keyof Tuple]: UnwrapIterable<Tuple[Key]> }
  >
;
type UnwrapIterable<Iter> =
  Iter extends Iterable<infer T>
    ? T
    : never
;

type _ = Assert<Equal<
  Zip<[Iterable<string>, Iterable<number>]>,
  Iterable<[string, number]>
>>;

37.7.3 Typing a function zipObj()

Function zipObj() is similar to zip(): It converts an object of iterables to an iterable of objects (source code of an implementation):

> zipObj({num: [1, 2, 3], str: ['a', 'b', 'c']})
[ {num: 1, str: 'a'}, {num: 2, str: 'b'}, {num: 3, str: 'c'} ]

The following utility type ZipObj computes a return type for it:

type ZipObj<Obj extends Record<string, Iterable<unknown>>> =
  Iterable<
    { [Key in keyof Obj]: UnwrapIterable<Obj[Key]> }
  >
;
type UnwrapIterable<Iter> =
  Iter extends Iterable<infer T>
    ? T
    : never
;

type _ = Assert<Equal<
  ZipObj<{a: Iterable<string>, b: Iterable<number>}>,
  Iterable<{a: string; b: number}>
>>;

37.7.4 util.promisify(): converting a callback-based function to a Promise-based one

The Node.js function util.promisify(cb) converts a function that returns its result via a callback to a function that returns it via a Promise. Its official type is long:

// 0 arguments
export function promisify<TResult>(
    fn: (callback: (err: any, result: TResult) => void) => void,
): () => Promise<TResult>;
export function promisify(
  fn: (callback: (err?: any) => void) => void
): () => Promise<void>;

// 1 argument
export function promisify<T1, TResult>(
    fn: (arg1: T1, callback: (err: any, result: TResult) => void) => void,
): (arg1: T1) => Promise<TResult>;
export function promisify<T1>(
  fn: (arg1: T1, callback: (err?: any) => void) => void
): (arg1: T1) => Promise<void>;

// 2 arguments
export function promisify<T1, T2, TResult>(
    fn: (arg1: T1, arg2: T2, callback: (err: any, result: TResult) => void) => void,
): (arg1: T1, arg2: T2) => Promise<TResult>;
export function promisify<T1, T2>(
    fn: (arg1: T1, arg2: T2, callback: (err?: any) => void) => void,
): (arg1: T1, arg2: T2) => Promise<void>;

// Etc.: up to 5 arguments

Let’s try to simplify it:

function promisify<Args extends any[], CB extends NodeCallback>(
  fn: (...args: [...Args, CB]) => void,
): (...args: Args) => Promise<ExtractResultType<CB>> {
  // ···
}
type NodeCallback =
  | ((err: any, result: any) => void)
  | ((err: any) => void)
;

//----- Test -----

function nodeFunc(
  arr: Array<string>,
  cb: (err: Error, str: string) => void
) {}
const asyncFunc = promisify(nodeFunc);
assertType<
  (arr: string[]) => Promise<string>
>(asyncFunc);

The previous code uses the following utility type:

type ExtractResultType<F extends NodeCallback> =
  F extends (err: any) => void
  ? void
  : F extends (err: any, result: infer TResult) => void
  ? TResult
  : never
;

//----- Test -----

type _ = [
  Assert<Equal<
    ExtractResultType<(err: Error, result: string) => void>,
    string
  >>,
  Assert<Equal<
    ExtractResultType<(err: Error) => void>,
    void
  >>,
];

37.8 Limitations of computing with tuples

There are constraints we can’t express via TypeScript’s type system. The following code is one example:

type Same<T> = {a: T, b: T};

function one<T>(obj: Same<T>) {}
// @ts-expect-error: Type 'string' is not assignable to type 'boolean'.
one({a: false, b: 'abc'}); // 👍 error

function many<A, B, C, D, E>(
  objs: [Same<A>, Same<B>]
      | [Same<A>, Same<B>, Same<C>]
      | [Same<A>, Same<B>, Same<C>, Same<D>]
      | [Same<A>, Same<B>, Same<C>, Same<D>, Same<E>,
        ...Array<Same<unknown>>]
) {}

many([
  {a: true, b: true},
  {a: 'abc', b: 'abc'},
  // @ts-expect-error: Type 'boolean' is not assignable to type 'number'.
  {a: 7, b: false} // 👍 error
]);

We’d like to express:

We can’t loop and introduce one variable per loop iteration. Therefore, we list the most common cases manually.

37.9 Sources of this chapter