bastianplsfix

Tuple types

A tuple type gives each position its own type and fixes the length. An array type says "any number of these"; a tuple says "exactly this, in this order". This entry is the syntax's three parts and one extra rule, the two ways to make a literal into a tuple, what spreading normalises to, what labels do and do not buy, and the recursion techniques that the last five entries have been promising to a page of their own.

Create programs/tuple-types.test.ts for this reference and keep it open, starting with the imports:

import { assertEquals, assertStrictEquals } from "@std/assert";
import { assertType, type IsExact } from "@std/testing/types";

Below the imports, add the types and the function the whole page shares:

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

type Row = [name: string, flagged?: boolean, ...scores: number[]];

type Point3 = [x: number, y: number, z: number];

function scale(point: Point3, by: number): Point3 {
const [x, y, z] = point;
return [x * by, y * by, z * by];
}

Follow the page as you add and revise the runnable examples below it.

exactly this, in this order

Deno.test("exactly this, in this order", () => {
const range: Interval = [0, 10];
const [start, end] = range;

// @ts-expect-error: Type '[number, number, number]' is not assignable to type 'Interval'.
const tooMany: Interval = [0, 10, 20];

// @ts-expect-error: Type 'string' is not assignable to type 'number'.
const wrongType: Interval = ["0", 10];

assertEquals([start, end], [0, 10]);
assertEquals(tooMany, [0, 10, 20]);
assertEquals(wrongType, ["0", 10]);
});
Check programs/tuple-types.test.ts
running 1 test from ./programs/tuple-types.test.ts
exactly this, in this order ... ok (278µs)

ok | 1 passed | 0 failed (1ms)

The first refusal's second line is the useful one, and it is quoted in full in the next step's capture for a sibling case: "Source has 3 element(s) but target allows only 2." Length is part of the type. start: and end: are labels, and they do less than they look like they do, which a later step gets to.

required, then optional, then rest

Deno.test("required, then optional, then rest", () => {
const full: Row = ["Ada", true, 1, 2, 3];
const short: Row = ["Ada", true];
const shortest: Row = ["Ada"];

// @ts-expect-error: Type '[]' is not assignable to type 'Row'.
const empty: Row = [];

// An optional element can only be left out at the end.
// @ts-expect-error: Type 'number' is not assignable to type 'boolean | undefined'.
const skipped: Row = ["Ada", 1, 2, 3];

assertEquals(full.length, 5);
assertEquals([short.length, shortest.length, empty.length], [2, 1, 0]);
assertEquals(skipped as unknown, ["Ada", 1, 2, 3]);
});
required, then optional, then rest ... ok (76µs)

The syntax has three parts and they come in one order: required elements, then optional elements, then at most one rest element, so Row accepts lengths one and up. The empty array is refused with "Source has 0 element(s) but target requires 1", and so is skipping the optional element to get at the rest: positions are counted from the left, so a number in position two is checked against boolean | undefined and nothing else. This is the same shape as a JavaScript parameter list, which is not a coincidence: a function's parameters are a tuple, and the same three parts appear there in the same order, as the parameters and arguments page covers.

a required element may follow a rest element

One rule the syntax adds: a required element may follow a rest element, which is legal and occasionally useful, while combining it with an optional element is not. Strip both shields in a copy of the file:

Deno.test("a required element may follow a rest element", () => {
type Ends = [...middle: boolean[], last: string];

const ends: Ends = [true, false, "last"];
const onlyTail: Ends = ["last"];

const noTail: Ends = [true, false];

type Broken = [first?: number, ...middle: boolean[], last: string];

assertEquals(ends, [true, false, "last"]);
assertEquals(onlyTail, ["last"]);
assertEquals(noTail as unknown, [true, false]);
});
Check programs/tuple-types.test.ts
TS2322 [ERROR]: Type '[true, boolean]' is not assignable to type 'Ends'.
Type at position 1 in source is not compatible with type at position 1 in target.
Type 'boolean' is not assignable to type 'string'.
const noTail: Ends = [true, false];
~~~~~~
at file:///programs/tuple-types.test.ts:53:9

TS1257 [ERROR]: A required element cannot follow an optional element.
type Broken = [first?: number, ...middle: boolean[], last: string];
~~~~~~~~~~~~
at file:///programs/tuple-types.test.ts:55:56

Found 2 errors.

error: Type checking failed.

The Ends refusal walks positions from the right, which is how the last position stays pinned to the end however many booleans come before it: that is how you type "a list and then a summary". The Broken line earns an empirical correction to the post this page is built from. The post calls it a parse error, a SyntaxError no @ts-expect-error can shield because the parser rejects the file before the checker sees it; in this toolchain the line parses fine, deno fmt accepts it, and the refusal arrives as an ordinary checker error, TS1257, which shields like any other. The fix it asks for is unchanged: a union of two tuple types, one with the optional element and one without. Shield both:

Deno.test("a required element may follow a rest element", () => {
type Ends = [...middle: boolean[], last: string];

const ends: Ends = [true, false, "last"];
const onlyTail: Ends = ["last"];

// @ts-expect-error: Type '[true, boolean]' is not assignable to type 'Ends'.
const noTail: Ends = [true, false];

// @ts-expect-error: A required element cannot follow an optional element.
type Broken = [first?: number, ...middle: boolean[], last: string];

assertEquals(ends, [true, false, "last"]);
assertEquals(onlyTail, ["last"]);
assertEquals(noTail as unknown, [true, false]);
});
a required element may follow a rest element ... ok (30µs)

an array literal is not a tuple until you say so

Deno.test("an array literal is not a tuple until you say so", () => {
const inferred = ["a", 1];
const frozen = ["a", 1] as const;
const satisfied = ["a", 1] satisfies [unknown, ...unknown[]];

assertType<IsExact<typeof inferred, Array<string | number>>>(true);
assertType<IsExact<typeof frozen, readonly ["a", 1]>>(true);
assertType<IsExact<typeof satisfied, [string, number]>>(true);

inferred.push("more");
satisfied.push("more");
assertEquals([inferred.length, frozen.length, satisfied.length], [3, 2, 3]);
});
an array literal is not a tuple until you say so ... ok (39µs)

Inference never produces a tuple, as the typing arrays page explains: ["a", 1] is an array of the union, because that is the type that lets you keep pushing. Two ways to ask for better. as const gives a readonly tuple of literal types, which is usually what you want and is covered on the read-only page; satisfies with a tuple shape gives a mutable tuple of widened types, the one to reach for when you need to keep mutating it, which the two push calls at the bottom pin from the value side. The pattern [unknown, ...unknown[]] is saying "at least one element", and it is the shortest way to ask for tuple inference without freezing anything.

spreading a tuple type, and what it normalises to

Deno.test("spreading a tuple type, and what it normalises to", () => {
type Spread1<T extends Array<unknown>> = [...T];
type Spread2<A extends Array<unknown>, B extends Array<unknown>> = [
...A,
...B,
];

// @ts-expect-error: A rest element type must be an array type.
type Bad<T> = [...T];

assertType<
IsExact<[true, ...["a", "b"], ...[1, 2], false], [
true,
"a",
"b",
1,
2,
false,
]>
>(true);

assertType<IsExact<Spread1<Array<string>>, Array<string>>>(true);
assertType<
IsExact<Spread2<["a", "b"], Array<number>>, ["a", "b", ...number[]]>
>(true);
assertType<
IsExact<Spread2<Array<string>, Array<number>>, Array<string | number>>
>(true);
assertType<
IsExact<
Spread2<Array<string>, [number?, boolean?]>,
Array<string | number | boolean | undefined>
>
>(true);
assertType<
IsExact<Spread2<[string?], [number]>, [string | undefined, number]>
>(true);
assertType<
IsExact<
Spread2<[boolean, ...number[]], [string, ...bigint[]]>,
[boolean, ...(string | number | bigint)[]]
>
>(true);

assertEquals(
[true, ...["a", "b"], ...[1, 2], false],
[true, "a", "b", 1, 2, false],
);
});
spreading a tuple type, and what it normalises to ... ok (56µs)

Spreading works the way it does in an array literal and composes tuple types out of other tuple types, which the first assertion and its run-time twin at the bottom say together. What is worth studying is what happens when the thing you spread is not a fixed tuple: the result gets normalised back into the three-part shape, and read in order, the six assertions reveal the rule. There can only be one rest element, so anything that would produce two gets merged into one whose element type is the union: spread two arrays and you get one array of both, put optional elements after an array and they are swallowed into it, and put an optional element before a required one and it becomes T | undefined, because a position that must be occupied cannot also be skippable. The one that catches people is the third: [...Array<string>, ...Array<number>] loses all structure and becomes Array<string | number>, and if you were expecting the strings to come first, nothing in the type system is tracking that. Spreading needs a constraint too, which the shielded Bad pins: "A rest element type must be an array type", so the parameter has to be constrained with extends Array<unknown> or extends ReadonlyArray<unknown> to be spreadable.

labels are documentation and nothing else

Predict that the labels distinguish Interval from a tuple labelled differently, by asserting the types are not identical:

Deno.test("labels are documentation and nothing else", () => {
assertType<IsExact<Interval, [number, number]>>(true);
assertType<IsExact<Interval, [first: number, second: number]>>(false);
assertType<
IsExact<
Parameters<(text: string, times: number) => void>,
[ignored: string, alsoIgnored: number]
>
>(true);

const range: Interval = [0, 10];
assertEquals(range, [0, 10]);
});
Check programs/tuple-types.test.ts
TS2345 [ERROR]: Argument of type 'false' is not assignable to parameter of type 'true'.
assertType<IsExact<Interval, [first: number, second: number]>>(false);
~~~~~
at file:///programs/tuple-types.test.ts:130:66

error: Type checking failed.

The checker refuses the false, because IsExact answers true: [start: number, end: number] is the same type as [number, number] and the same type as [first: number, second: number]. Labels are invisible to the type system: you cannot compare them, derive anything from them, or rely on them for compatibility, and the Parameters assertion says the same about the names in a function signature. If two positions need names that mean something, use an object type. Correct the prediction:

Deno.test("labels are documentation and nothing else", () => {
assertType<IsExact<Interval, [number, number]>>(true);
assertType<IsExact<Interval, [first: number, second: number]>>(true);
assertType<
IsExact<
Parameters<(text: string, times: number) => void>,
[ignored: string, alsoIgnored: number]
>
>(true);

const range: Interval = [0, 10];
assertEquals(range, [0, 10]);
});
labels are documentation and nothing else ... ok (25µs)

a rest parameter with a tuple type is a parameter list

Labels are not useless, and this is the one place the names show through. Predict what draw prints when the optional label is left out, reasoning that a missing optional element is undefined:

Deno.test("a rest parameter with a tuple type is a parameter list", () => {
function draw(...args: [x: number, y: number, label?: string]): string {
const [x, y, label = "?"] = args;
return `${label}@${x},${y}`;
}

assertStrictEquals(draw(1, 2, "here"), "here@1,2");
assertStrictEquals(draw(1, 2), "undefined@1,2");
});
Check programs/tuple-types.test.ts
running 7 tests from ./programs/tuple-types.test.ts
...
a rest parameter with a tuple type is a parameter list ... FAILED (8ms)

ERRORS

a rest parameter with a tuple type is a parameter list => ./programs/tuple-types.test.ts:142:6
error: AssertionError: Values are not strictly equal.

[Diff] Actual / Expected

- ?@1,2
+ undefined@1,2

FAILURES

a rest parameter with a tuple type is a parameter list => ./programs/tuple-types.test.ts:142:6

FAILED | 6 passed | 1 failed (10ms)

error: Test failed

The missing element really is undefined, and the destructuring default catches it before it reaches the string, which is what the = "?" is for. Correct the prediction:

Deno.test("a rest parameter with a tuple type is a parameter list", () => {
function draw(...args: [x: number, y: number, label?: string]): string {
const [x, y, label = "?"] = args;
return `${label}@${x},${y}`;
}

assertStrictEquals(draw(1, 2, "here"), "here@1,2");
assertStrictEquals(draw(1, 2), "?@1,2");
});
a rest parameter with a tuple type is a parameter list ... ok (57µs)

A rest parameter with a tuple type is a parameter list: draw is called with two or three arguments, not with an array, and an editor shows x, y, and label because the labels are there. That is the technique the overloading page recommends in place of overload signatures, and labels are what make it readable.

tuples and noUncheckedIndexedAccess

Deno leaves this flag off, and it is the one place where turning it on changes what a tuple is worth. Verified against a scratch config:

{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}
const arr: Array<string> = ["a", "b", "c"];
const fromArray: string = arr[1];

const tuple: [string, string, string] = ["a", "b", "c"];
const fromTuple: string = tuple[1];

const row: [name: string, flagged?: boolean, ...scores: number[]] = ["Ada"];
const optional: boolean = row[1];
const rest: number = row[2];

console.log(fromArray, fromTuple, optional, rest);
Check positions.ts
TS2322 [ERROR]: Type 'string | undefined' is not assignable to type 'string'.
Type 'undefined' is not assignable to type 'string'.
const fromArray: string = arr[1];
~~~~~~~~~
at file:///flagged-project/positions.ts:2:7

TS2322 [ERROR]: Type 'boolean | undefined' is not assignable to type 'boolean'.
Type 'undefined' is not assignable to type 'boolean'.
const optional: boolean = row[1];
~~~~~~~~
at file:///flagged-project/positions.ts:8:7

TS2322 [ERROR]: Type 'number | undefined' is not assignable to type 'number'.
Type 'undefined' is not assignable to type 'number'.
const rest: number = row[2];
~~~~
at file:///flagged-project/positions.ts:9:7

Found 3 errors.

error: Type checking failed.

arr[1] on an Array<string> is string | undefined, because the checker never knows how long an array is. tuple[1] on a [string, string, string] passes clean, because the length is part of the type. And a rest position or an optional position is refused again, which is the interesting one: the flag is not being cautious there, it is being right, because those positions genuinely might be missing and a fixed position genuinely cannot be. So the flag rewards the exact thing tuples are for, and a codebase that has it on has a much stronger reason to reach for a tuple over an array.

getting the indices back out of keyof

Deno.test("getting the indices back out of keyof", () => {
type IndexKeys<T extends ReadonlyArray<unknown>> = Extract<
keyof T,
`${number}`
>;

type AsNumber<Str> = Str extends `${infer N extends number}` ? N : never;

type Indices<T extends ReadonlyArray<unknown>> = AsNumber<keyof T>;

assertType<IsExact<IndexKeys<["a", "b"]>, "0" | "1">>(true);
assertType<IsExact<Indices<["a", "b"]>, 0 | 1>>(true);

// Which is needed because a number literal vanishes into `number`.
assertType<IsExact<number | 0 | 1, number>>(true);

assertEquals(Object.keys(["a", "b"]), ["0", "1"]);
});
getting the indices back out of keyof ... ok (17µs)

keyof a tuple is a mess: number, "length", "push", every other array method, and one stringified index per element, as the keyof and indexed access page shows. Intersecting with `${number}` keeps only the indices, and parsing them with infer N extends number turns them back into numbers; both halves come from the template literal types page. The last assertion explains why the indices are strings in the first place: number | 0 | 1 collapses to number, so number literal keys would be unrecoverable, while string ones survive alongside number and can be filtered back out.

mapping a tuple gives a tuple

Deno.test("mapping a tuple gives a tuple", async () => {
type Wrap<T> = { [K in keyof T]: Promise<T[K]> };

type Unwrap<T extends ReadonlyArray<unknown>> = {
-readonly [K in keyof T]: Awaited<T[K]>;
};

assertType<
IsExact<Wrap<[x: 1, y: 2]>, [x: Promise<1>, y: Promise<2>]>
>(true);
assertType<
IsExact<
Unwrap<readonly [Promise<number>, Promise<string>]>,
[number, string]
>
>(true);

const settled = await Promise.all([Promise.resolve(1), Promise.resolve("a")]);

assertType<IsExact<typeof settled, [number, string]>>(true);
assertEquals(settled, [1, "a"]);
});
mapping a tuple gives a tuple ... ok (29µs)

A mapped type over a tuple gives back a tuple of the same length, with the labels intact, and it quietly visits only the indices rather than "length" and the methods: the special case, from the mapped types page, that makes all of this usable. Unwrap is the type behind Promise.all, close enough to quote: the -readonly lets it accept an as const array and hand back a mutable tuple, and Awaited unwraps each element. Beside it is the real thing: Promise.all of a promise of a number and a promise of a string is a promise of [number, string], position by position. You have relied on that, and this is how it is written; the promise combinators page is the entry for what it does at run time. Renaming the keys with as breaks all of this and gives you an object with the old indices as properties, which the mapped types page shows.

a tuple of tuples is a table you can query

Deno.test("a tuple of tuples is a table you can query", () => {
const currencies = [
["EUR", "€", "euro"],
["USD", "$", "dollar"],
["GBP", "£", "pound"],
] as const;

type Code = (typeof currencies)[number][0];
type Sign = (typeof currencies)[number][1];

assertType<IsExact<Sign, "€" | "$" | "£">>(true);

const code: Code = "GBP";
// @ts-expect-error: Type '"CHF"' is not assignable to type '"EUR" | "USD" | "GBP"'.
const invented: Code = "CHF";

const found = currencies.find(([abbreviation]) => abbreviation === code);
assertEquals(found, ["GBP", "£", "pound"]);
assertStrictEquals(invented, "CHF");

const counters = [
{ name: "upperRoman", pattern: /^[IVXLCDM]+$/ },
{ name: "lowerLatin", pattern: /^[a-z]$/ },
{ name: "decimal", pattern: /^[0-9]+$/ },
] as const satisfies ReadonlyArray<{ name: string; pattern: RegExp }>;

type CounterName = (typeof counters)[number]["name"];

assertType<
IsExact<CounterName, "upperRoman" | "lowerLatin" | "decimal">
>(true);

const chosen: CounterName = "decimal";
const counter = counters.find(({ name }) => name === chosen);
assertStrictEquals(counter, counters[2]);
});
a tuple of tuples is a table you can query ... ok (59µs)

(typeof currencies)[number] is the union of the rows, and [0] on top of that is the union of first columns: two indexed accesses and you have a type per column, from data written once. A Map gives you fast lookup by one key; this gives you a type for every column and lookup by any of them, which is the right trade for a table of a few dozen rows that never changes. The same works with objects instead of rows, and as const satisfies is the combination worth memorising: as const keeps "decimal" from widening to string, so CounterName is a real union, and satisfies checks each entry against the shape, so a typo in pattern is caught where you wrote it rather than where you used it. Neither alone does both, which the object types page covers for the operator in general.

taking a tuple apart with infer

Deno.test("taking a tuple apart with infer", () => {
type First<T extends ReadonlyArray<unknown>> = T extends
readonly [infer F, ...unknown[]] ? F : never;

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

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

assertType<IsExact<First<["a", "b", "c"]>, "a">>(true);
assertType<IsExact<Last<["a", "b", "c"]>, "c">>(true);
assertType<IsExact<Rest<["a", "b", "c"]>, ["b", "c"]>>(true);

const list = ["a", "b", "c"] as const;
const [head, ...tail] = list;

assertType<IsExact<typeof head, "a">>(true);
assertType<IsExact<typeof tail, ["b", "c"]>>(true);

assertStrictEquals(head, "a");
assertEquals(tail, ["b", "c"]);
});
taking a tuple apart with infer ... ok (26µs)

unknown[] is the wildcard: it matches any number of elements you do not care about, and Last works because a rest element is allowed at the start of a pattern, which is the one place that syntax rule earns its keep. Beside them, the value-level version: const [head, ...tail] = list is the same operation, same shape, one level down, and the two IsExacts on head and tail say the value-level move computes the same types. If you can write the destructuring, you can write the infer, which is the point the conditional types page makes about infer generally.

recursion is the only loop, and an accumulator is the only counter

Deno.test("recursion is the only loop, and an accumulator is the only counter", () => {
type Repeat<
Length extends number,
Item,
Acc extends Array<unknown> = [],
> = Acc["length"] extends Length ? Acc : Repeat<Length, Item, [...Acc, Item]>;

type WithoutEmpty<T extends ReadonlyArray<string>> = T extends readonly [
infer Head extends string,
...infer Tail extends ReadonlyArray<string>,
] ? Head extends "" ? WithoutEmpty<Tail> : [Head, ...WithoutEmpty<Tail>]
: [];

assertType<IsExact<Repeat<3, "x">, ["x", "x", "x"]>>(true);
assertType<IsExact<Repeat<0, "x">, []>>(true);

assertType<
IsExact<WithoutEmpty<["", "a", "", "b"]>, ["a", "b"]>
>(true);

// The run-time filter, which is one call and closes the gaps too.
assertEquals(["", "a", "", "b"].filter((item) => item !== ""), ["a", "b"]);
});
recursion is the only loop, and an accumulator is the only counter ... ok (19µs)

WithoutEmpty is the filter the mapped types page could not write: take the head, decide whether to keep it, and rebuild the tail recursively, and the result is a real tuple with no gaps, the thing a mapped type cannot do because it visits keys instead of building a sequence. Repeat is the other technique: there is no way to count down at the type level, because there is no arithmetic, so you count up by growing an accumulator and comparing Acc["length"] to the target. Every "make me a tuple of N things" type is this, and every range type is this with Acc["length"] pushed in instead of the item. Both are bounded by the same ceiling, a thousand instantiations and TS2589 past it, which the template literal types page works out precisely: generous for parameter lists and useless for anything data-shaped.

what a tuple type cannot say

Deno.test("what a tuple type cannot say", () => {
type Same<T> = { a: T; b: T };

function pairOf<T>(_value: Same<T>): void {}

pairOf({ a: 1, b: 2 });

// @ts-expect-error: Type 'string' is not assignable to type 'number'.
pairOf({ a: 1, b: "two" });

assertEquals(scale([1, 2, 3], 2), [2, 4, 6]);
});
what a tuple type cannot say ... ok (35µs)

One object, two properties, and a constraint that they match: that works. Now ask for a list of such objects, each with its own independent pair type, and there is no way to write it: you would need one type variable per element for a number of elements you do not know, and a tuple type cannot introduce variables. The honest workaround is to enumerate the common lengths as a union of tuple types and accept a ceiling, which is what library authors do and why Promise.all used to have a signature per arity. Knowing where the ceiling is saves the afternoon you would spend looking for the clever version. The scale pin closes the entry where the practice starts: three coordinates in a known order is a tuple worth having, and three unrelated settings is not.

The whole entry

Run the whole reference suite:

Check programs/any-unknown-never.test.ts
Check programs/arrays.test.ts
Check programs/assignment.test.ts
Check programs/async-functions.test.ts
Check programs/async-iteration.test.ts
Check programs/branching.test.ts
Check programs/branding.test.ts
Check programs/buffers-and-views.test.ts
Check programs/classes-as-values.test.ts
Check programs/classes.test.ts
Check programs/closures.test.ts
Check programs/conditional-types.test.ts
Check programs/conversion-and-coercion.test.ts
Check programs/dates-and-times.test.ts
Check programs/designing-error-types.test.ts
Check programs/destructuring.test.ts
Check programs/enum-patterns.test.ts
Check programs/enums.test.ts
Check programs/equality.test.ts
Check programs/errors-and-exceptions.test.ts
Check programs/function-types.test.ts
Check programs/functions.test.ts
Check programs/generators.test.ts
Check programs/interfaces-and-type-aliases.test.ts
Check programs/iterables-and-iterators.test.ts
Check programs/iterator-helpers.test.ts
Check programs/json.test.ts
Check programs/keyof-and-indexed-access.test.ts
Check programs/loops.test.ts
Check programs/mapped-types.test.ts
Check programs/maps.test.ts
Check programs/matching-and-replacing.test.ts
Check programs/module-specifiers.test.ts
Check programs/modules.test.ts
Check programs/mutating-arrays.test.ts
Check programs/nothing-twice.test.ts
Check programs/numbers.test.ts
Check programs/object-types.test.ts
Check programs/objects-as-dictionaries.test.ts
Check programs/objects.test.ts
Check programs/ordering-and-sorting.test.ts
Check programs/overloading.test.ts
Check programs/parameters-and-arguments.test.ts
Check programs/private-class-members.test.ts
Check programs/promise-combinators.test.ts
Check programs/promises.test.ts
Check programs/prototypes-and-inheritance.test.ts
Check programs/read-only.test.ts
Check programs/regular-expressions.test.ts
Check programs/scope-and-declarations.test.ts
Check programs/sentinels.test.ts
Check programs/sets.test.ts
Check programs/strings.test.ts
Check programs/subclassing.test.ts
Check programs/symbols.test.ts
Check programs/tagged-templates.test.ts
Check programs/template-literal-types.test.ts
Check programs/testing-types.test.ts
Check programs/text-and-characters.test.ts
Check programs/the-event-loop.test.ts
Check programs/the-value-of-this.test.ts
Check programs/transforming-arrays.test.ts
Check programs/truthiness.test.ts
Check programs/tuple-types.test.ts
Check programs/typed-arrays.test.ts
Check programs/typing-arrays.test.ts
Check programs/typing-classes.test.ts
Check programs/unicode-in-patterns.test.ts
Check programs/unions-and-narrowing.test.ts
Check programs/values-and-references.test.ts
Check programs/weak-collections.test.ts
Check programs/what-a-type-is.test.ts
running 10 tests from ./programs/any-unknown-never.test.ts
...
running 13 tests from ./programs/arrays.test.ts
...
running 9 tests from ./programs/assignment.test.ts
...
running 10 tests from ./programs/async-functions.test.ts
...
running 11 tests from ./programs/async-iteration.test.ts
...
running 10 tests from ./programs/branching.test.ts
...
running 9 tests from ./programs/branding.test.ts
...
running 12 tests from ./programs/buffers-and-views.test.ts
...
running 10 tests from ./programs/classes-as-values.test.ts
...
running 11 tests from ./programs/classes.test.ts
...
running 6 tests from ./programs/closures.test.ts
...
running 14 tests from ./programs/conditional-types.test.ts
...
running 11 tests from ./programs/conversion-and-coercion.test.ts
...
running 13 tests from ./programs/dates-and-times.test.ts
...
running 10 tests from ./programs/designing-error-types.test.ts
...
running 14 tests from ./programs/destructuring.test.ts
...
running 12 tests from ./programs/enum-patterns.test.ts
...
running 7 tests from ./programs/enums.test.ts
...
running 11 tests from ./programs/equality.test.ts
...
running 10 tests from ./programs/errors-and-exceptions.test.ts
...
running 12 tests from ./programs/function-types.test.ts
...
running 11 tests from ./programs/functions.test.ts
...
running 12 tests from ./programs/generators.test.ts
...
running 9 tests from ./programs/interfaces-and-type-aliases.test.ts
...
running 14 tests from ./programs/iterables-and-iterators.test.ts
...
running 12 tests from ./programs/iterator-helpers.test.ts
...
running 11 tests from ./programs/json.test.ts
...
running 14 tests from ./programs/keyof-and-indexed-access.test.ts
...
running 14 tests from ./programs/loops.test.ts
...
running 12 tests from ./programs/mapped-types.test.ts
...
running 15 tests from ./programs/maps.test.ts
...
running 15 tests from ./programs/matching-and-replacing.test.ts
...
running 6 tests from ./programs/module-specifiers.test.ts
...
running 12 tests from ./programs/modules.test.ts
...
running 10 tests from ./programs/mutating-arrays.test.ts
...
running 11 tests from ./programs/nothing-twice.test.ts
...
running 15 tests from ./programs/numbers.test.ts
...
running 15 tests from ./programs/object-types.test.ts
...
running 14 tests from ./programs/objects-as-dictionaries.test.ts
...
running 13 tests from ./programs/objects.test.ts
...
running 12 tests from ./programs/ordering-and-sorting.test.ts
...
running 8 tests from ./programs/overloading.test.ts
...
running 11 tests from ./programs/parameters-and-arguments.test.ts
...
running 11 tests from ./programs/private-class-members.test.ts
...
running 11 tests from ./programs/promise-combinators.test.ts
...
running 11 tests from ./programs/promises.test.ts
...
running 12 tests from ./programs/prototypes-and-inheritance.test.ts
...
running 12 tests from ./programs/read-only.test.ts
...
running 13 tests from ./programs/regular-expressions.test.ts
...
running 9 tests from ./programs/scope-and-declarations.test.ts
...
running 8 tests from ./programs/sentinels.test.ts
...
running 13 tests from ./programs/sets.test.ts
...
running 10 tests from ./programs/strings.test.ts
...
running 11 tests from ./programs/subclassing.test.ts
...
running 10 tests from ./programs/symbols.test.ts
...
running 8 tests from ./programs/tagged-templates.test.ts
...
running 12 tests from ./programs/template-literal-types.test.ts
...
running 14 tests from ./programs/testing-types.test.ts
...
running 10 tests from ./programs/text-and-characters.test.ts
...
running 9 tests from ./programs/the-event-loop.test.ts
...
running 10 tests from ./programs/the-value-of-this.test.ts
...
running 13 tests from ./programs/transforming-arrays.test.ts
...
running 9 tests from ./programs/truthiness.test.ts
...
running 13 tests from ./programs/tuple-types.test.ts
exactly this, in this order ... ok (285µs)
required, then optional, then rest ... ok (47µs)
a required element may follow a rest element ... ok (25µs)
an array literal is not a tuple until you say so ... ok (33µs)
spreading a tuple type, and what it normalises to ... ok (29µs)
labels are documentation and nothing else ... ok (15µs)
a rest parameter with a tuple type is a parameter list ... ok (50µs)
getting the indices back out of keyof ... ok (15µs)
mapping a tuple gives a tuple ... ok (19µs)
a tuple of tuples is a table you can query ... ok (43µs)
taking a tuple apart with infer ... ok (16µs)
recursion is the only loop, and an accumulator is the only counter ... ok (14µs)
what a tuple type cannot say ... ok (23µs)
running 14 tests from ./programs/typed-arrays.test.ts
...
running 10 tests from ./programs/typing-arrays.test.ts
...
running 12 tests from ./programs/typing-classes.test.ts
...
running 11 tests from ./programs/unicode-in-patterns.test.ts
...
running 13 tests from ./programs/unions-and-narrowing.test.ts
...
running 13 tests from ./programs/values-and-references.test.ts
...
running 9 tests from ./programs/weak-collections.test.ts
...
running 7 tests from ./programs/what-a-type-is.test.ts
...

ok | 809 passed | 0 failed (893ms)

Thirteen tests, and the practice is short. Use a tuple for a fixed row, a pair, or an argument list: coordinates, an interval, a key and value, the parameters of a function you are transforming, the cases where position genuinely carries meaning. Use an object type the moment the names matter, because labels will not save you: they are invisible to the type system, and a reader who mixes up start and end gets no help at all. Reach for as const satisfies when a list is both data and a type, since as const keeps the literals, satisfies checks the shape, and (typeof list)[number] gives you the union. Constrain to ReadonlyArray<unknown>, not Array<unknown>, because otherwise an as const value cannot be passed in, which is the single most common friction in tuple-typed helpers. Let a mapped type do it if a mapped type can, because it keeps tuple-ness, keeps labels, and cannot go wrong, and reach for recursion only for filtering, flattening, and building, with the run-time equivalent in a test beside it. And do not compute with tuples where data would do: a thousand-step limit, no arithmetic, and error messages that name a type you cannot read, so if a list is long enough to need a loop, it probably wants to be a value with a validator rather than a type.