skip to content

In TypeScript, how do you use `infer` inside tuple patterns to extract a tuple type's first element type, its remaining elements, and its last element type?

level: middleimportance: should knowfreq 40%

answer

  1. destructure the type, not the value
  2. rest position captures a whole tuple
  3. a rest may lead as well as trail
  4. open arrays might be empty
  5. readonly in the pattern matches both

basics

~10 s

Match the tuple against a tuple pattern containing infer: [infer H, ...unknown[]] captures the head, [unknown, ...infer R] captures the tail as a tuple, and [...unknown[], infer L] captures the last element.

solid answer

~50 s

Tuple patterns let `infer` destructure positionally, the same way array destructuring does for values. `T extends readonly [infer H, ...unknown[]] ? H : never` grabs the head; `T extends readonly [unknown, ...infer R] ? R : never` grabs everything after it, and `R` comes back as a **tuple type**, not a union; `T extends readonly [...unknown[], infer L] ? L : never` grabs the last element, because a rest element may sit at the front of a pattern as well as at the end. Two behaviours catch people out. An open array type such as `string[]` does *not* match `[infer H, ...unknown[]]`, since an array may be empty and so is not assignable to a tuple that requires at least one element. And an empty tuple `[]` falls to the false branch of every one of these patterns, which is usually what you want.

code

typescript · 16 lines
typescript
type Head<T extends readonly unknown[]> =
  T extends readonly [infer H, ...unknown[]] ? H : never;

type Tail<T extends readonly unknown[]> =
  T extends readonly [unknown, ...infer R] ? R : never;

type Last<T extends readonly unknown[]> =
  T extends readonly [...unknown[], infer L] ? L : never;

type A = Head<[string, number, boolean]>; // string
type B = Tail<[string, number, boolean]>; // [number, boolean]
type C = Last<[string, number, boolean]>; // boolean
type D = Head<string[]>;                  // never

const tail: Tail<[string, number, boolean]> = [1, true];
console.log(tail.length);

go deeper

for a junior

Recognise that a tuple pattern with infer mirrors array destructuring, and that the result is a type used in annotations — there is no array being taken apart when the program runs.

for a middle

Write head, tail and last patterns without hesitating, and say clearly that a rest infer captures a tuple type rather than a union of its elements.

for a senior

Explain why an open array type fails a tuple pattern, and design the false branches so a mismatch produces a clear error rather than a never that quietly infects everything downstream.

for a principal

Judge how far a codebase should go with tuple-level type manipulation, since these helpers compound quickly into checker cost and errors that only their author can read.

## Tuples are matchable shapes A tuple type such as `[string, number, boolean]` records both the element types and their positions. That makes it a rich pattern to match against, and `infer` inside a tuple pattern is how you take it apart. The syntax deliberately mirrors JavaScript array destructuring, which is why it reads naturally even to someone seeing it for the first time. ```ts type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never; type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer R] ? R : never; type Last<T extends readonly unknown[]> = T extends readonly [...unknown[], infer L] ? L : never; type A = Head<[string, number]>; // string type B = Tail<[string, number]>; // [number] type C = Last<[string, number]>; // number ``` ## The rest position captures a tuple The single most common misunderstanding is what `...infer R` produces. It is a **tuple type**, preserving order and arity — `Tail<[1, 2, 3]>` is `[2, 3]`, not `2 | 3` and not `(2 | 3)[]`. That is what makes these patterns composable: the output of `Tail` is itself a tuple you can feed straight back into another tuple pattern, which is how list-like type helpers are built up. A rest element in a pattern is not restricted to the end. `[...unknown[], infer L]` is a legal pattern with a *leading* rest, and it is the idiomatic way to reach the final element. Getting the order backwards — writing `[infer L, ...unknown[]]` and calling it `Last` — is the classic slip. ## Open arrays do not match tuple patterns ```ts type X = Head<string[]>; // never ``` `string[]` describes an array of unknown length, possibly empty, so it is not assignable to a pattern that demands at least one element. The conditional therefore takes the false branch. This is correct rather than a limitation: the type system genuinely does not know that a `string[]` has a first element, and a helper that pretended otherwise would be unsound. If you want to accept both, write the array case as a separate branch: ```ts type FirstOf<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : T extends readonly (infer U)[] ? U | undefined : never; ``` The `| undefined` there is deliberate honesty about a possibly-empty array. ## Empty tuples and the false branch `Head<[]>`, `Tail<[]>` and `Last<[]>` all fall to the false branch, because `[]` satisfies none of the three patterns. Returning `never` for those cases is the usual choice, and it gives a visible failure downstream. If a helper needs to keep working on an empty input — for example a tail that should stay a tuple — return `[]` from the false branch instead of `never`. ## Constraining the parameter Writing `T extends readonly unknown[]` on the type parameter itself is worth doing. It gives callers an immediate, readable error when they pass a non-tuple, instead of silently handing them `never` from deep inside the conditional. It also documents intent: this helper is about list-shaped types. The `readonly` in both the constraint and the pattern widens what matches. A mutable `[string, number]` is assignable to `readonly [string, number]`, so the readonly form accepts both; the reverse is not true, so a pattern written without `readonly` silently rejects readonly tuples — a frequent cause of an unexplained `never`. ## It is all erased None of this touches runtime. There is no array being taken apart; the compiler is matching type shapes during checking and emits nothing. A helper called `Tail` produces no code and cannot be called — it exists only where types are written.

  • Why does `Tail<[1, 2, 3]>` give you `[2, 3]` rather than `2 | 3`?
    Because a rest element in a tuple pattern binds a tuple, preserving both order and arity. That is what makes these helpers chainable — the result is another tuple you can match again. You would only get a union by indexing the result afterwards, for example `Tail<[1, 2, 3]>[number]`.
  • How would you make a head-extracting helper also work for an open array type such as `string[]`?
    Add a second conditional branch after the tuple pattern: if `T` matches `readonly (infer U)[]`, return `U | undefined`. The `undefined` is the honest part — the type system cannot know an open array is non-empty, so promising a definite first element would be a lie the compiler would then trust.
  • What does the `readonly` in `T extends readonly [infer H, ...unknown[]]` buy you?
    It widens what matches. A mutable tuple is assignable to its readonly counterpart, so the readonly pattern accepts both forms, while a pattern written without it silently rejects readonly tuples and drops them into the false branch. Unexplained `never` results from readonly inputs are usually this.

saying these in an interview costs you the question

  • Thinks a rest infer yields a union of element types
  • Expects tuple patterns to match an open array type
  • Places the rest element first when extracting the last item
  • Assumes tuple destructuring in types happens at runtime
  • Forgets that an empty tuple matches none of these patterns

context