skip to content

In the TypeScript tuple type `[string, number?, ...boolean[]]`, what do the `?` and the `...` mean, and what is that tuple's `length` type?

level: middleimportance: should knowfreq 42%

answer

  1. one may be missing, the rest is open-ended
  2. optional read includes undefined
  3. how many sizes can the compiler name
  4. rest element makes length plain number
  5. required may not follow optional

basics

~20 s

The ? marks an optional element that may be omitted, and reading it yields number | undefined. The ... is a rest element allowing zero or more booleans at the end. Because the rest element makes the size open-ended, length is typed number.

solid answer

~40 s

The first element is required. `number?` is an **optional element**: the value may stop there, and a read of index 1 is typed `number | undefined`. `...boolean[]` is a **rest element**: any remaining positions must be booleans, and there may be none. So `['a']`, `['a', 1]` and `['a', 1, true, false]` all satisfy it. The `length` type follows from that — with no rest element, `[string, number?]` has length `1 | 2`, a union of literals; add a rest element and the length becomes plain `number`, because the compiler can no longer name the possibilities. The compiler also enforces ordering rules: a required element may not follow an optional one, a tuple may hold at most one rest element, and no optional element may follow the rest element.

code

typescript · 12 lines
typescript
type Row = [string, number?, ...boolean[]];

const a: Row = ['r1'];
const b: Row = ['r2', 9];
const c: Row = ['r3', 9, true, false];

const maybe = b[1]; // number | undefined
console.log(maybe ?? 0);

type Pair = [string, number?];
type PairLen = Pair['length']; // 1 | 2
type RowLen = Row['length'];   // number — the rest element opens it up

go deeper

for a junior

Recognise the two modifiers on sight: ? marks an element that may be omitted, ...T[] allows any number of further elements of type T at that position.

for a middle

Explain the derived types — an optional read is T | undefined, length is a union of literals without a rest element and plain number with one — and state the ordering rules the compiler enforces.

for a senior

Show when the flexibility is worth the lost length guarantee, and recognise the point where a shape needing optional plus rest elements should really be an options object rather than a positional list.

for a principal

Own the API contract: a positional signature encoded as a tuple is hard to extend without breaking callers. Decide where your codebase draws the line between positional argument tuples and named-parameter objects.

## Beyond fixed-length tuples A plain tuple type such as `[string, number]` describes an exact shape. Two modifiers relax that in controlled ways: the optional marker `?` and the rest element `...`. Both are compile-time only — the emitted JavaScript is an ordinary array either way. ## Optional elements An element written with `?` may be absent: ```typescript type Pair = [string, number?]; const a: Pair = ['a']; // ok — second element omitted const b: Pair = ['a', 1]; // ok ``` Two consequences matter in interviews. First, reading the optional position gives you `number | undefined`, so under `strictNullChecks` you must handle the missing case before using it: ```typescript const p: [string, number?] = ['a']; const n = p[1]; // number | undefined // const m = n + 1; // Error: object is possibly 'undefined' ``` Second, the length is no longer a single literal. `Pair['length']` is `1 | 2` — a union of the sizes the type permits. That union is genuinely useful: a `switch (t.length)` over it narrows which positions exist. Ordering is enforced. A **required element may not follow an optional one**, exactly as with function parameters: `[string?, number]` is rejected, because there would be no way to write a value that omits the first element while supplying the second. ## Rest elements A rest element absorbs an open-ended run of positions: ```typescript type Row = [string, ...number[]]; const r1: Row = ['id']; const r2: Row = ['id', 1, 2, 3]; ``` The rules the compiler enforces: at most **one** rest element per tuple, and an optional element may not follow it. What surprises people is that the rest element does **not** have to be last. A tuple may put fixed elements after the rest run: ```typescript type Trailing = [...string[], number]; // any number of strings, then one number const t: Trailing = ['a', 'b', 7]; ``` This is what makes tuple types able to describe argument lists like "a callback always comes last". ## The length type, precisely The rule is simple once you see it as "can the compiler enumerate the possible sizes?": - `[string, number]` → `length` is `2` - `[string, number?]` → `length` is `1 | 2` - `[string, number?, ...boolean[]]` → `length` is `number` Once a rest element is present the set of sizes is infinite, so the compiler falls back to `number` and you lose out-of-range index errors along the rest run. That is a real tradeoff: a rest element buys flexibility and spends the strongest guarantee a tuple has. ## Reading positions inside the rest run Within the fixed prefix, index reads are precise: for `[string, number?, ...boolean[]]`, `t[0]` is `string`. Beyond the fixed part, an index read is typed by the rest element's type — reads land in `boolean` territory rather than erroring, because the compiler cannot know where the array actually ends. This is the same open-endedness that plain arrays have, confined to the tail of the tuple. ## Where this shows up in real code The common uses are argument lists and heterogeneous records: ```typescript // A logging call: a message, an optional severity, then any number of tags. type LogArgs = [message: string, level?: 'warn' | 'error', ...tags: string[]]; declare function log(...args: LogArgs): void; log('started'); log('slow query', 'warn'); log('failed', 'error', 'db', 'retry'); ``` Because the tuple sits in the rest-parameter position, the optional element behaves like an optional parameter and the rest element like a rest parameter — one type expresses the whole signature. ## Common mistakes The first is expecting `t.length` on a rest-bearing tuple to still be a literal and building a `switch` on it. The second is assuming `?` means "may be `undefined`" rather than "may be absent": `['a', undefined]` is accepted for `[string, number?]` because the optional element's type includes `undefined`, but the two situations are not the same for code that checks `t.length`. The third is trying to write two rest elements, or an optional element after the rest — both are compile errors, and the fix is almost always to model the value as an object instead, because a shape that needs those is no longer positional in any readable sense.

  • Must a rest element be the last element of a tuple type?
    No. TypeScript allows a rest element in a leading or middle position, so `[...string[], number]` describes any number of strings followed by exactly one number. The restrictions are that a tuple may have only one rest element and that no optional element may follow it. This is what lets tuples model argument lists whose callback comes last.
  • What is the difference between `[string, number?]` and `[string, number | undefined]`?
    `[string, number?]` permits the value `['a']` — the element may be absent, and the length type is `1 | 2`. `[string, number | undefined]` requires two elements, so you must write `['a', undefined]` explicitly and the length is `2`. Both read as `number | undefined` at index 1, so the difference shows up in construction and in length-based logic.
  • Why does a rest element cost you out-of-range index errors?
    Those errors depend on the compiler knowing the exact size. With a rest element the set of valid sizes is infinite, so `length` degrades to `number` and index reads past the fixed prefix are typed by the rest element rather than rejected. You keep precision only across the fixed positions.

saying these in an interview costs you the question

  • Thinks length stays a literal type when a rest element is present
  • Says ? means the element may be undefined rather than absent
  • Believes a rest element must always be last
  • Tries to put a required element after an optional one
  • Assumes a tuple can carry two rest elements

context