Skip to content
aviral gupta

// B5.4 · ~30 min · Beginner

Array<T>, ReadonlyArray and tuples

After this lesson you can protect arrays from changes with readonly, and type data with fixed positions as tuples.

Lesson 4 of 5 in B5 Object types

You will be able to

  • Write array types as T[] or Array<T>, and forbid changes with readonly T[] or ReadonlyArray<T>
  • Explain why a mutable array fits a readonly one but not the other way round, and fix error TS4104
  • Type fixed positions with tuples, including optional, rest and readonly elements and as const
  1. Warm-up · Activity 1 of 7

    Warm-up from JavaScript: copy is another name for scores. What does this print?

    const scores = [70, 85];
    const copy = scores;
    copy.push(99);
    console.log(scores.length);
  2. Predict · Activity 2 of 7

    Predict before you read on: scores is a readonly array. Which line does tsc reject?

    function report(scores: readonly number[]): number {
      const best = scores.toSorted((a, b) => b - a);
      best.push(0);
      scores.push(0);
      return best[0];
    }
  3. Practice · Activity 3 of 7

    Fill in the gap so that total promises not to change the prices: the // @ts-expect-error line expects an error, and the program prints 5.5.

    function total(prices: ____ number[]): number {
      // @ts-expect-error: total must not change the prices
      prices.push(0);
      let sum = 0;
      for (const p of prices) sum += p;
      return sum;
    }
    
    console.log(total([2, 3.5]));
    function total(prices: number[]): number {
  4. Practice · Activity 4 of 7

    Two assignments between a mutable and a readonly array. Which line does tsc reject?

    let editable: string[] = ["a", "b"];
    let locked: readonly string[] = ["c"];
    
    locked = editable;
    editable = locked;
  5. Practice · Activity 5 of 7

    The Handbook's coordinate with an optional third element. Which types do z and n have at the comment?

    function setCoordinate(coord: [number, number, number?]) {
      const [x, y, z] = coord;
      const n = coord.length;
      // here
      return x + y + (z ?? 0) + n;
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. pair is a tuple of exactly two elements. What happens when you run npx tsc, then node main.ts?

    const pair: [string, number] = ["Ada", 36];
    pair.push(1815);
    console.log(pair.length, pair);
  7. Apply · Activity 7 of 7

    Mini-task. Write minMax(values), which takes a readonly number[] and returns the smallest and largest value as a readonly [number, number]. Call it with an ordinary array such as [11.5, 19, 16.5], destructure the result into low and high, and print 11.5 to 19. Then try values.push(0) inside minMax, and an assignment to the result's first element, and read both errors. Check it in the editor, or with npx tsc and node main.ts.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

Temperature readings

A Reading is a readonly tuple: time first, temperature second. The list of readings is readonly too, and warmest promises in its parameter not to change it, so it sorts a copy with toSorted. range takes a tuple whose unit may be left out; week has a label followed by any number of values; units comes from as const. Run npx tsc, then node main.ts. Then add readings.push(["21:00", 14]); and week[0] = 1; and run tsc again.

main.ts

// A reading has two fixed positions: the time, then the temperature.
type Reading = readonly [string, number];

// Once loaded, the readings must not change: readonly on the array too.
const readings: readonly Reading[] = [
  ["06:00", 11.5],
  ["12:00", 19],
  ["18:00", 16.5]
];

// readonly in the parameter: this function promises not to change the list.
function warmest(list: readonly Reading[]): Reading {
  const sorted = list.toSorted((a, b) => b[1] - a[1]); // a new, mutable copy
  return sorted[0];
}

// An optional last element: the unit may be left out.
function range(r: [number, number, string?]): string {
  const [min, max, unit = "C"] = r;
  return min + " to " + max + " " + unit + " (" + r.length + " elements)";
}

// A rest element: a label, then any number of values.
const week: [string, ...number[]] = ["Mon", 11.5, 19, 16.5];
const [label, ...values] = week;

// as const: a readonly tuple of literal types.
const units = ["C", "F"] as const;

const [time, temp] = warmest(readings);
console.log("warmest: " + temp + " at " + time);
console.log(range([11.5, 19]));
console.log(range([52.7, 66.2, "F"]));
console.log(label + ": " + values.length + " values");
console.log(units.join(" or "), readings.length);

Run it with

npx tsc
node main.ts

Output

warmest: 19 at 12:00
11.5 to 19 C (2 elements)
52.7 to 66.2 F (3 elements)
Mon: 3 values
C or F 3
  • toSorted returns a new array, so warmest leaves readings in their order.
  • Destructuring a tuple gives each part its own type: time is a string, temp a number.
  • r.length is 2 or 3, depending on whether the unit was passed.
  • readings.push(…) would be error TS2339, because readings is readonly.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

A median that leaves the input alone

median sorts the array it is given, so the caller's array changes order, and the // @ts-expect-error in noEdits is unused, because Values allows push. Make Values a readonly array of numbers. tsc then rejects values.sort: sort a copy with toSorted instead. Also handle an even number of values: the median is then the average of the two middle values, so median([4, 1, 3, 2]) is 2.5. Run the tests: the first one is the type-check.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    The readonly modifier goes in front of the array type: export type Values = readonly number[];

  2. Hint 2

    toSorted works like sort but returns a new array: const sorted = values.toSorted((a, b) => a - b);

  3. Hint 3

    With mid = Math.floor(sorted.length / 2), an even length uses (sorted[mid - 1] + sorted[mid]) / 2.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export type Values = readonly number[];

export function median(values: Values): number {
  const sorted = values.toSorted((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  if (sorted.length % 2 === 0) return (sorted[mid - 1] + sorted[mid]) / 2;
  return sorted[mid];
}

function noEdits(values: Values) {
  // @ts-expect-error: Values is read-only
  values.push(0);
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export type Values = number[];

export function median(values: Values): number {
  values.sort((a, b) => a - b);
  return values[Math.floor(values.length / 2)];
}

function noEdits(values: Values) {
  // @ts-expect-error: Values is read-only
  values.push(0);
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {median} from './main.ts';

test('the median of an odd number of values is the middle one', () => {
  const got = median([5, 1, 3]);
  assert.equal(got, 3, `median([5, 1, 3]) gave ${got}`);
});

test('the median of an even number of values is the average of the middle two', () => {
  const got = median([4, 1, 3, 2]);
  assert.equal(got, 2.5, `median([4, 1, 3, 2]) gave ${got}`);
});

test("median leaves the caller's array in its order", () => {
  const values = [5, 1, 3];
  median(values);
  assert.deepEqual(values, [5, 1, 3], `after median the array is ${JSON.stringify(values)}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

Score rows as tuples

A Row is a player's name followed by any number of scores, but (string | number)[] does not say which comes first, so destructuring gives string | number and tsc reports error TS2322 for player. Make Row a tuple with a rest element, so that the // @ts-expect-error on the empty row finds its error. Make bestScore return a readonly [string, number], so that writing to top[1] is reported too. A player without scores gets 0 as the best score.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    A rest element goes last in the brackets: [string, ...number[]] is a string followed by any number of numbers.

  2. Hint 2

    Put readonly in front of the return type: function bestScore(row: Row): readonly [string, number] {

  3. Hint 3

    Math.max() with no arguments gives -Infinity, so check scores.length === 0 first and return [player, 0].

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export type Row = [string, ...number[]];

export function bestScore(row: Row): readonly [string, number] {
  const [player, ...scores] = row;
  if (scores.length === 0) return [player, 0];
  return [player, Math.max(...scores)];
}

// @ts-expect-error: a row starts with the player's name
const empty: Row = [];

const top = bestScore(["Ada", 9]);
// @ts-expect-error: the result is read-only
top[1] = 10;
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export type Row = (string | number)[];

export function bestScore(row: Row): [string, number] {
  const [player, ...scores] = row;
  return [player, Math.max(...scores)];
}

// @ts-expect-error: a row starts with the player's name
const empty: Row = [];

const top = bestScore(["Ada", 9]);
// @ts-expect-error: the result is read-only
top[1] = 10;

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {bestScore} from './main.ts';

test('the best score comes with the player', () => {
  const got = bestScore(['Ada', 3, 9, 4]);
  assert.deepEqual(got, ['Ada', 9], `bestScore(['Ada', 3, 9, 4]) gave ${JSON.stringify(got)}`);
});

test('negative scores count too', () => {
  const got = bestScore(['Cy', -2, -5]);
  assert.deepEqual(got, ['Cy', -2], `bestScore(['Cy', -2, -5]) gave ${JSON.stringify(got)}`);
});

test('a player without scores gets 0', () => {
  const got = bestScore(['Bob']);
  assert.deepEqual(got, ['Bob', 0], `bestScore(['Bob']) gave ${JSON.stringify(got)}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

Calling push on a readonly array

const tags: readonly string[] = ["news", "tech"];
tags.push("sport");

What tsc or Node.js prints

main.ts(2,6): error TS2339: Property 'push' does not exist on type 'readonly string[]'.

Why, and the fix

A readonly array type has no methods that change it, so push is simply not there. If the list really must grow, make it a string[]. If it must stay as it is, build a new array: const more = [...tags, "sport"]; or tags.concat("sport"). Both leave tags unchanged and give you a new, mutable array.

Handing an as const array to a mutable type

const roles = ["admin", "editor"] as const;
const list: string[] = roles;

What tsc or Node.js prints

main.ts(2,7): error TS4104: The type 'readonly ["admin", "editor"]' is 'readonly' and cannot be assigned to the mutable type 'string[]'.

Why, and the fix

as const makes roles a readonly tuple, and a string[] would allow push and sort on it. If you only read the list, type it readonly string[] as well. If you need to change it, copy it: const list: string[] = [...roles];. The copy is a new array, so roles stays as it was.

Expecting an array literal to be a tuple

const pair = ["Ada", 36];
const person: [string, number] = pair;

What tsc or Node.js prints

main.ts(2,7): error TS2322: Type '(string | number)[]' is not assignable to type '[string, number]'.

Why, and the fix

Without a tuple type, tsc infers an ordinary array, (string | number)[], which could have any length and any order of strings and numbers. The next line says so: Target requires 2 element(s) but source may have fewer. Annotate where the value is created, const pair: [string, number] = ["Ada", 36], or use as const if it never changes.

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

readonly T[]: an array you can read but not change

string[] is short for Array<string>; both mean the same. Mind the brackets with unions: (string | number)[] holds both kinds, string | number[] is a string or an array of numbers. readonly string[], long form ReadonlyArray<string>, removes every method that changes the array: names.push("x") is error TS2339: Property 'push' does not exist on type 'readonly string[]'., and so are sort, reverse and splice. names[0] = "x" is error TS2542. Reading, slice, map and toSorted still work, and the last three return a new, mutable array. There is no ReadonlyArray constructor (TS2693). Like readonly properties, this is shallow and exists only for tsc: the objects inside can still change.

Mutable fits readonly, not the other way round

A parameter of type readonly number[] accepts any number array, because the function promises not to change it. The Handbook: "we can pass any array into that function without worrying that it will change its contents." The other direction is refused: assigning a readonly string[] to a string[] is error TS4104: The type 'readonly string[]' is 'readonly' and cannot be assigned to the mutable type 'string[]'., since the new name would allow push. That is the difference from readonly properties (B5.1), where tsc allows both directions. If you need a changeable copy, make one: values.slice() or [...values]. readonly does not freeze anything: the original array may still change under another name.

Tuples: fixed positions with their own types

[string, number] is an array with exactly two elements: a string, then a number. pair[2] is error TS2493, and ["a", 1, 2] does not fit (Source has 3 element(s) but target allows only 2.). A plain array literal is never inferred as a tuple: const pair = ["Ada", 36] is (string | number)[]. A ? marks optional elements at the end: [number, number, number?] has the length 2 | 3. A rest element allows more: [string, ...number[]] is a string followed by any number of numbers. A tuple still has push, so prefer readonly [string, number]: then pair[0] = "x" is error TS2540. as const on an array literal gives exactly such a readonly tuple.

Sources

Last reviewed October 4, 2026