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.
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.
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.
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.
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
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”.
// 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
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
TypesExplain 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.
Completion is saved in this browser only. It does not measure mastery.