Tuples & Mapped Types

The Course
•
5 min read

A tuple describes a fixed set of positions, such as readonly [string, number]. Unlike a general array of string|number, it tells the checker which type belongs at each position. Destructuring can then preserve the relationship between a label and its count.

as const asks the checker to retain literal values and readonly structure for an expression. It does not freeze JavaScript data at runtime. For an existing value needing a broader tuple contract, an explicit annotation is often clearer.

A mapped type transforms each known key: type ReadonlyFields<T> = { readonly [K in keyof T]: T[K] }. It preserves each property’s own value type while adding readonly at the immediate level. Built-in Readonly already supplies this behavior, so prefer it in application code. Writing one small mapped type helps you read library declarations.

Mapped types do not loop over runtime objects or create copies. Keep type transformations separate from value transformations. Complex conditional or recursive types can wait until a concrete API requires them.

Read the mapped type one piece at a time: keyof T is the union of T’s property names; K in keyof T considers each name; T[K] preserves that property’s value type; readonly prevents assignment through the resulting type. This is type-level syntax, not a JavaScript for loop. The mapped-types handbook explains the same pattern.

Compare const fixed = ["Books", 2] as const with the broader annotation below: fixed has type readonly ["Books", 2], so its first position is specifically the literal “Books”. The annotated summary allows any string in that position. Both are read without changing the runtime array.

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Use Check types first; execution is allowed only when the real compiler accepts the source.

Predict, then runTypeScript
type ReadonlyFields<T> = { readonly [K in keyof T]: T[K] };

const fixed = ["Books", 2] as const;
const summary: readonly [string, number] = fixed;
const [label, count] = summary;
const book: ReadonlyFields<{
  title: string;

  pages: number;
}> = {
  title: "Types",
  pages: 120
};

console.log(label, count);
console.log(book.title);

Enable JavaScript to edit and type-check this example.

Check your prediction
Books 2
Types

Read the compiler error

A tuple promises the type at each position, not just an unordered set of types.

Find and fix the type errorTypeScript
const pair: readonly [string, number] = [25, "Types"];

console.log(pair);

Enable JavaScript to edit and type-check this example.

Check your diagnosis

Place the string first and number second. A cast would not reorder the runtime array.

Fixed positions preserve more information than an array union

A tuple describes the roles of individual positions. An array of string|number says each position might contain either type, while readonly [string,number] fixes a label first and a count second. That difference affects both safe access and construction.

Readonly and literal preservation are independent of runtime freezing. A tuple can protect positional assignments through its type while still containing a shared mutable object. Use the smallest contract that captures the relationship callers actually depend on.

Fixed positions preserve more information than an array union: traceTypeScript
const pair: readonly [string, number] = ["Books", 2];
const [label, count] = pair;

console.log(label.toUpperCase());
console.log(count + 1);

Enable JavaScript to edit and type-check this example.

Check your prediction
BOOKS
3

Destructuring retains each position’s type. No typeof check is needed because the tuple contract already establishes which field occupies that position.

Fixed positions preserve more information than an array union: contrastTypeScript
type Box = { value: number };

const box: Box = { value: 1 };
const pair: readonly [string, Box] = ["Count", box];

box.value = 2;
console.log(pair[1].value);

Enable JavaScript to edit and type-check this example.

Check your prediction
2

The tuple’s second slot cannot be reassigned through pair, but the nested Box is not readonly. A wrapper permission is not a deep ownership guarantee.

Try the rule on new inputs

Compare a fixed tuple with a general array containing the same values. Try reversing the construction positions and inspect the diagnostic. Then identify exactly which nested permissions remain writable.

Your turn

Exercise

Tuples & Mapped Types: practise

Define a readonly tuple [string, number] for a topic and minutes. Destructure it and print Types: 25. Then define a custom ReadonlyFields helper and create a readonly record with title:“Types”.

Your workspaceTypeScript
// Use a tuple annotation to fix positions; do not assert away a mismatched value.

Enable JavaScript to edit and type-check this example.

Hint 1: the idea

The type at each position is known independently.

Hint 2: a first step

Write readonly [string, number] and destructure into two names.

Show one solution
One possible solutionTypeScript
type ReadonlyFields<T> = { readonly [K in keyof T]: T[K] };

const session: readonly [string, number] = ["Types", 25];
const [topic, minutes] = session;
const book: ReadonlyFields<{ title: string }> = { title: "Types" };

console.log(`${topic}: ${minutes}`);
console.log(book.title);

Enable JavaScript to edit and type-check this example.

Types: 25
Types

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Try reversing the tuple values or writing book.title after construction and inspect the diagnostics. Compare ReadonlyFields with built-in Readonly. Explain why neither one removes fields or deep-freezes nested data.

Was this article helpful?

Your response is saved on this device.