TypeScript tuples, readonly, and optional chaining

Return fixed-shape pairs, protect arrays from change, and read nested optional values safely.

TypeScript
function minMax(values: readonly number[]): [number, number] {
  return [Math.min(...values), Math.max(...values)];
}

const [low, high] = minMax([4, 9, 2, 7]);
console.log(low, high);

interface Address {
  city?: string;
}
interface Person {
  name: string;
  address?: Address;
  nickname?: string;
  visits: number;
}

const people: Person[] = [
  { name: "Ada", address: { city: "London" }, nickname: "A", visits: 0 },
  { name: "Bo", visits: 3 },
];

for (const person of people) {
  const city = person.address?.city ?? "unknown";
  const label = person.nickname ?? person.name;
  const visits = person.visits || 10;
  const visitsKept = person.visits ?? 10;
  console.log(label + " / " + city + " / " + visits + " / " + visitsKept);
}
Output
2 9
A / London / 10 / 0
Bo / unknown / 3 / 3

Explanation

A tuple type such as [string, number] describes an array with a fixed length and a known type at each position. It is handy for returning two values from a function and unpacking them with destructuring. A readonly array type, written readonly number[], forbids push, pop, and element assignment, which documents and enforces that a function will not change its input.

Optional chaining a?.b?.c stops and returns undefined if any link is null or undefined, instead of throwing. The nullish coalescing operator ?? supplies a default only when the left side is null or undefined, unlike ||, which also replaces 0 and the empty string. Together they make code that handles missing data short and safe.

Explore the dry run
Guided walkthroughReady
1function minMax(values: readonly number[]): [number, number] {
2 return [Math.min(...values), Math.max(...values)];
3}
4
5const [low, high] = minMax([4, 9, 2, 7]);
6console.log(low, high);
7
8interface Address {
9 city?: string;
10}
11interface Person {
12 name: string;
13 address?: Address;
14 nickname?: string;
15 visits: number;
16}
17
18const people: Person[] = [
19 { name: "Ada", address: { city: "London" }, nickname: "A", visits: 0 },
20 { name: "Bo", visits: 3 },
21];
22
23for (const person of people) {
24 const city = person.address?.city ?? "unknown";
25 const label = person.nickname ?? person.name;
26 const visits = person.visits || 10;
27 const visitsKept = person.visits ?? 10;
28 console.log(label + " / " + city + " / " + visits + " / " + visitsKept);
29}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
The input is readonly, so the function cannot modify it, and the result is a pair of numbers.—
—
The tuple is unpacked into two variables.—
—
The smallest and largest values.—
2 9
For Bo, address is undefined, so the whole chain gives undefined and ?? supplies the default.—
2 9
|| treats 0 as missing, so Ada's zero visits become 10.—
2 9
?? replaces only null and undefined, so 0 is kept.—
2 9
Ada shows the difference between || and ??. Bo has no address, so the city is unknown.—
2 9
A / London / 10 / 0
Bo / unknown / 3 / 3

Cost and common mistakes

minMax is O(n); the property accesses are O(1).

  • Math.min(...values) with a very large array can exceed the argument limit. Use a loop for huge arrays.
  • readonly is a compile-time promise only. It does not stop code that casts it away, and it does not freeze the array at run time.
  • ?. only guards against null and undefined. A value of the wrong type still causes an error.

Try it yourself

What does person.address?.city return when address exists but city is missing?

Reveal answer and reasoning

undefined, because the city property is optional and not set; ?? "unknown" then supplies the default.

Further reading