Arrays, Tuples & Readonly Arrays
Reviewed & published by Brayan K
An array (T[] or Array<T>) holds many values of one type, a tuple ([string, number]) is a fixed-length list with a type per position, and readonly versions prevent mutation.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Type arrays two equivalent ways: T[] and Array<T>
- Keep element types through map, filter, and push
- Build tuples: fixed-length, one type per position
- Add readability with named tuple members
- Use readonly arrays and tuples to prevent mutation
- Capture a flexible tail with rest elements in tuples
💡 Real-World Analogy
An array is a shopping list — any number of items, all "things to buy". A tuple is a form field row: slot 1 is always the name, slot 2 always the age, in that exact order and never longer. A readonly array is a laminated reference card — you can read it all day, but you can't scribble new entries on it; to change anything you print a fresh card.
1. Arrays: T[] vs Array<T>
An array holds any number of values of a single type. You can write the type as number[] or as the generic Array<number> — they're identical. The payoff is that array methods preserve the element type: map over a number[] gives a number[], and trying to push a value of the wrong type is a compile error.
let scores: number[] = [95, 87, 92];
let scores2: Array<number> = [95, 87, 92]; // identical type
const doubled = scores.map(n => n * 2); // doubled: number[]
// scores.push("oops"); // ❌ 'string' is not assignable to 'number'// An ARRAY holds any number of values of the SAME type.
// Two IDENTICAL ways to write the type:
// let scores: number[] = [95, 87];
// let scores: Array<number> = [95, 87];
// They mean exactly the same thing — pick whichever reads better to you.
let scores = [95, 87, 92];
console.log("scores:", scores); // scores: [ 95, 87, 92 ]
console.log("first:", scores[0]); // first: 95
console.log("length:", scores.length); // length: 3
scores.push(100); // adding another number is fine
console.log("after push:", scores); // after push: [ 95, 87, 92, 100 ]
// Array methods keep the element type:
const doubled = scores.map((n) => n * 2);
console.log("doubled:", doubled); // doubled: [ 190, 174, 184, 200 ]
const passing = scores.filter((n) => n >= 90);
console.log("passing:", passing); // passing: [ 95, 92, 100 ]
// In a .ts file: let scores: number[] = [...] (or Array<number>)
// scores.push("oops"); // ❌ Argument of type 'string' not assignable to 'number'2. Tuples & Named Members
A tuple is a fixed-length array where each position has its own type: [string, number] means "a string then a number, exactly two elements". Named tuple members ([x: number, y: number]) attach labels purely for readability — they don't change the runtime value. Tuples are ideal for fixed pairs and for returning several related values from one function.
let user: [string, number] = ["Ada", 36];
// user = [36, "Ada"]; // ❌ order matters — types must line up
type Point = [x: number, y: number]; // named members (labels only)
function divmod(a: number, b: number): [number, number] {
return [Math.floor(a / b), a % b];
}
const [q, r] = divmod(17, 5); // q: number, r: number// A TUPLE is a FIXED-LENGTH array with a type for EACH position.
// In TypeScript: let user: [string, number] = ["Ada", 36];
// Position 0 must be a string, position 1 must be a number.
let user = ["Ada", 36];
console.log("name:", user[0]); // name: Ada (the string slot)
console.log("age: ", user[1]); // age: 36 (the number slot)
// NAMED TUPLE MEMBERS add labels for readability (names are TS-only):
// type Point = [x: number, y: number];
let point = [10, 20];
console.log("x =", point[0], "y =", point[1]); // x = 10 y = 20
// Tuples are perfect for fixed pairs — like a key/value or a return of two things:
function divmod(a, b) {
return [Math.floor(a / b), a % b]; // [quotient, remainder]
}
const [q, r] = divmod(17, 5);
console.log("quotient:", q, "remainder:", r); // quotient: 3 remainder: 2
// In a .ts file:
// let user: [string, number] = ["Ada", 36];
// type Point = [x: number, y: number];
// function divmod(a: number, b: number): [number, number] { ... }3. Readonly Arrays/Tuples & Rest Elements
Prefix with readonly to forbid mutation: readonly number[] (or ReadonlyArray<number>) removes push, pop, and index assignment at the type level, so you build a new array instead of changing the old one. A rest element in a tuple — [command: string, ...flags: string[]] — gives a fixed start followed by a flexible, typed tail.
const a: readonly number[] = [1, 2, 3];
// a.push(4); // ❌ Property 'push' does not exist on 'readonly number[]'
const bigger = [...a, 4]; // ✅ build a NEW array
const t: readonly [string, number] = ["Ada", 36];
// t[1] = 0; // ❌ Cannot assign to '1' because it is a read-only property
type Args = [command: string, ...flags: string[]]; // rest element in a tuple// READONLY arrays/tuples can't be mutated (no push/pop/assignment) — a
// compile-time guarantee. In TypeScript:
// const a: readonly number[] = [1, 2, 3];
// const t: readonly [string, number] = ["Ada", 36];
// At runtime they're normal arrays; readonly is enforced by the COMPILER.
// (a.push(4) would be a TS error: "Property 'push' does not exist on readonly...")
const config = [200, 404, 500];
console.log("can still READ:", config[1]); // can still READ: 404
const next = [...config, 503]; // build a NEW array instead of mutating
console.log("new array:", next); // new array: [ 200, 404, 500, 503 ]
console.log("original untouched:", config);// original untouched: [ 200, 404, 500 ]
// REST elements in a tuple capture "the rest" with a known leading shape:
// type Args = [command: string, ...flags: string[]];
function run(command, ...flags) {
return command + " [" + flags.join(", ") + "]";
}
console.log(run("build")); // build []
console.log(run("build", "--fast", "--ci"));// build [--fast, --ci]
// In a .ts file:
// const a: readonly number[] = [1, 2, 3];
// type Args = [command: string, ...flags: string[]];🎯 Your Turn
Write labelled, which reads a [label, value] tuple. Fill in the two blanks marked ___, then run it.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// 'labelled' takes a tuple [label, value] and returns "label: value".
// In TS: function labelled(pair: [string, number]): string
function labelled(pair) {
const label = pair[___]; // 👉 index 0 — the string
const value = pair[___]; // 👉 index 1 — the number
return label + ": " + value;
}
console.log(labelled(["score", 95])); // score: 95
console.log(labelled(["age", 36])); // age: 36
// And an array of numbers — sum them:
const nums = [1, 2, 3, 4];
console.log("sum:", nums.reduce((a, b) => a + b, 0)); // sum: 10
// ✅ Expected output:
// score: 95
// age: 36
// sum: 10Common Errors (and the fix)
- ❌ "Type 'string' is not assignable to type 'number'" on push — wrong element type for the array. ✅ Push a value of the array's element type, or widen the type to a union.
- ❌ "Source has 3 element(s) but target allows only 2" — a tuple has a fixed length. ✅ Match the exact number of elements the tuple type declares.
- ❌ Tuple elements in the wrong order — [36, "Ada"] for [string, number]. ✅ Put each value in the position its type expects.
- ❌ "Property 'push' does not exist on type 'readonly ...'" — you tried to mutate a readonly array. ✅ Build a new array with the spread operator instead.
Pro Tips
- 💡 Pick one array syntax (T[] is most common) and use it consistently across your codebase.
- 💡 Reach for a tuple whenever order and slot-meaning are fixed — it documents intent far better than a loose array.
- 💡 Name tuple members in shared types so editors show x and y instead of [0] and [1].
- 💡 Default to readonly for data you don't intend to change — it makes accidental mutation a compile error.
Mini-Challenge: Point Toolkit
No blanks this time — just a brief and a starting outline. Build the tuple-based point helpers yourself, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: an (x, y) point toolkit using tuples
// In TypeScript: type Point = [x: number, y: number];
//
// 1. Write addPoints(a, b) that returns a NEW tuple [a.x + b.x, a.y + b.y]
// (use index 0 for x and index 1 for y).
// 2. Write distanceFromOrigin(p) = Math.hypot(p[0], p[1]).
// 3. Log:
// addPoints([1, 2], [3, 4]) -> [ 4, 6 ]
// distanceFromOrigin([3, 4]) -> 5
// const [x, y] = addPoints([0, 0], [5, 12]); then distanceFromOrigin([x, y]) -> 13
//
// ✅ Expected output:
// [ 4, 6 ]
// 5
// 13
// your code here🎉 Lesson Complete
- ✅ T[] and Array<T> are identical ways to type a list of one type
- ✅ Array methods (map, filter) preserve the element type
- ✅ A tuple is fixed-length with a type per position; order matters
- ✅ Named tuple members add labels for readability only
- ✅ readonly arrays/tuples block mutation at compile time; rebuild with spread
- ✅ Rest elements give a tuple a fixed start and a flexible typed tail
Practice quiz
How are number[] and Array<number> related?
- They are completely different types
- Array<number> allows mixed types
- They are exactly the same type, two syntaxes
- number[] is a tuple, Array<number> is not
Answer: They are exactly the same type, two syntaxes. number[] and Array<number> are identical - just two ways to write the same array type. Pick one and stay consistent.
What does the tuple type [string, number] describe?
- Exactly two elements: a string then a number
- An array of strings and numbers in any order
- An object with string and number keys
- A union of string and number
Answer: Exactly two elements: a string then a number. A tuple is a fixed-length array with a type per position: index 0 must be a string, index 1 must be a number.
When you map over a number[], what is the type of the result?
- any[]
- unknown[]
- object[]
- number[]
Answer: number[]. Array methods preserve the element type, so mapping a number[] gives a number[] (assuming the callback returns numbers).
What do named tuple members like [x: number, y: number] add?
- Runtime property names on the array
- Labels for readability only - no runtime change
- Automatic validation of values
- A conversion to an object
Answer: Labels for readability only - no runtime change. Named tuple members attach labels purely for readability and editor hints; they don't change the runtime value at all.
What does readonly do to an array at the type level?
- Removes mutating methods like push and index assignment
- Freezes the array at runtime
- Makes every element a string
- Limits the array to three elements
Answer: Removes mutating methods like push and index assignment. readonly removes push, pop, splice, and index assignment at the type level - a compile-time guarantee. The runtime array is still mutable.
How should you change a readonly number[] to add an element?
- Call .push() - it works on readonly arrays
- Cast it to any first
- Build a new array with the spread operator
- Use the delete keyword
Answer: Build a new array with the spread operator. You can't mutate a readonly array; build a new one instead, e.g. const bigger = [...a, 4].
What does a rest element in a tuple like [command: string, ...flags: string[]] mean?
- Exactly two strings
- A fixed leading string then any number of trailing strings
- An array that can never grow
- A tuple of unknown length only
Answer: A fixed leading string then any number of trailing strings. A rest element gives a fixed, typed start followed by a flexible typed tail - here one string then any number of string flags.
Why would you use a tuple instead of a plain array?
- When you have any number of same-typed items
- Tuples are always faster at runtime
- Arrays cannot hold objects
- When the length is fixed and each position has a specific meaning
Answer: When the length is fixed and each position has a specific meaning. Use a tuple when length is fixed and each slot means something (like [x, y]); use an array for any number of same-typed items.
For a tuple typed [string, number], why does [36, "Ada"] cause an error?
- Tuples can't hold numbers
- The element types are in the wrong order
- Tuples must have at least three elements
- Numbers must come last in every tuple
Answer: The element types are in the wrong order. Order matters in a tuple: position 0 must be a string and position 1 a number, so the values must line up with their declared types.
Which type is identical to readonly number[]?
- Array<number>
- number[]
- ReadonlyArray<number>
- Readonly<number>
Answer: ReadonlyArray<number>. readonly number[] and ReadonlyArray<number> are two spellings of the same read-only array type.
Continue this course
- Previous: Function Overloads & this Types
- Next: Checkpoint: The Type System — Combine unions, generics and tuples in a build — then a quiz
- Quick reference: TypeScript cheat sheet › Basic Types
Frequently asked questions
Is T[] different from Array<T>?
No — number[] and Array<number> are exactly the same type, just two syntaxes. T[] is shorter and the most common; Array<T> reads more like other generics and can be clearer when the element type itself is complex. Pick one style and stay consistent within a file or project.
When should I use a tuple instead of an array?
Use a tuple when the length is fixed and each position has a specific meaning and type — an (x, y) coordinate, an [key, value] pair, or a function returning two related things. Use a plain array when you have any number of same-typed items. Tuples document the exact shape; arrays document 'a list of'.
What does 'readonly' do to an array or tuple?
It removes the mutating methods (push, pop, splice, index assignment) at the type level, so the compiler stops you changing the array. The runtime array is still mutable — readonly is a compile-time contract. To genuinely change a readonly array, build a NEW one with the spread operator instead of mutating it.
Can tuples have a variable number of elements?
Yes, via a rest element. A tuple like [command: string, ...flags: string[]] requires a leading string and then allows any number of trailing strings. This gives you a fixed, typed beginning with a flexible tail — perfect for command-and-arguments style signatures.