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);// B5.4 · ~30 min · Beginner
After this lesson you can protect arrays from changes with readonly, and type data with fixed positions as tuples.
You will be able to
Warm-up · Activity 1 of 7
const scores = [70, 85];
const copy = scores;
copy.push(99);
console.log(scores.length);Predict · Activity 2 of 7
function report(scores: readonly number[]): number {
const best = scores.toSorted((a, b) => b - a);
best.push(0);
scores.push(0);
return best[0];
}Practice · Activity 3 of 7
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]));Practice · Activity 4 of 7
let editable: string[] = ["a", "b"];
let locked: readonly string[] = ["c"];
locked = editable;
editable = locked;Practice · Activity 5 of 7
function setCoordinate(coord: [number, number, number?]) {
const [x, y, z] = coord;
const n = coord.length;
// here
return x + y + (z ?? 0) + n;
}Brain teaser · Activity 6 of 7
const pair: [string, number] = ["Ada", 36];
pair.push(1815);
console.log(pair.length, pair);Apply · Activity 7 of 7
Check your work against this list
Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.
Worked example
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.tsOutput
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 3Tab 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.
Exercise 1 of 2
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.
The readonly modifier goes in front of the array type: export type Values = readonly number[];
toSorted works like sort but returns a new array: const sorted = values.toSorted((a, b) => a - b);
With mid = Math.floor(sorted.length / 2), an even length uses (sorted[mid - 1] + sorted[mid]) / 2.
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);
}
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.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsExercise 2 of 2
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.
A rest element goes last in the brackets: [string, ...number[]] is a string followed by any number of numbers.
Put readonly in front of the return type: function bestScore(row: Row): readonly [string, number] {
Math.max() with no arguments gives -Infinity, so check scores.length === 0 first and return [player, 0].
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;
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.tsRun the checks (needs learnrun.js in the same folder):
npx tsc
node --testDownload learnrun.jsconst 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.
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.
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
5 questions, no hints. Score 80% or more to complete the lesson.
Finish every activity above to unlock the exit ticket.