Tuples
A row of lockers with one sign works when all the items are of one kind. But some data is built differently. Take an ID card: the first field holds a name, the second an age. There are exactly two fields, their order never changes, and each holds its own kind of thing: the name is a string, the age a number. A guard reads the first field as the name and does not check whether a name is there.
In JavaScript such pairs are often kept in an array: ["Аня", 30]. But TypeScript sees an ordinary array in this literal:
const pair = ["Аня", 30];
const first = pair[0];
console.log(first.toUpperCase()); // ✗error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.You know that the name sits at position zero. TypeScript does not: for the literal ["Аня", 30] it inferred (string | number)[], an array in which any element is a string or a number. So to TypeScript pair[0] is a string | number, and a number has no toUpperCase method.
Why is that? An array may change: an element gets added, the array gets sorted. TypeScript does not guess that you meant a pair, and infers a type that stays true after such changes too.
A type with a slot for each value
A tuple is an array of a fixed length where each position has its own type. A tuple type is written as a list of types in square brackets: [string, number] reads "a string at position 0, a number at position 1".
const user: [string, number] = ["Аня", 30];
console.log(user[0].toUpperCase()); // АНЯ
console.log(user[1].toFixed(1)); // 30.0
console.log(user.length); // 2Let's go through the example line by line:
- The annotation
[string, number]says thatuseris a tuple of two positions. user[0]is astring, sotoUpperCaseworks and printsАНЯ.user[1]is anumber, andtoFixed(1)prints30.0.- The tuple's length is known in advance:
user.lengthhas the type2, and the program prints2.
The literal itself did not change; it is the same ["Аня", 30]. The annotation changed: you get a tuple only when you tell TypeScript about it yourself.
Past the end of a tuple
An ID card has no third field, and the tuple [string, number] has no position 2. tsc does not let the read user[2] through:
error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.The message means "the tuple [string, number] of length 2 has no element at index 2". In JavaScript such a read would silently return undefined.
The main thing in this lesson is where a tuple comes from. On its own, TypeScript infers a literal as an array, and a value becomes a tuple through an annotation. Step through the program and compare the type of pair with the type of user, then the types of their elements:
1const pair = ["Аня", 30];editor hoverpair: (string | number)[]2const first = pair[0];3const user: [string, number] = ["Аня", 30];4const who = user[0];5const age = user[1];6const extra = user[2];
From the literal, TypeScript inferred an array in which any element is a string or a number.
The value in pair and in user is the same, yet the types differ: without an annotation it is an array, with one it is a tuple.
Destructuring a tuple
Destructuring is a handy way to take a tuple apart: each variable gets the type of its position. This way a function can return two values at once:
function minMax(nums: number[]): [number, number] {
return [Math.min(...nums), Math.max(...nums)];
}
const [low, high] = minMax([3, 9, 1]);
console.log(low, high); // 1 9Let's see what happens here:
- The result type
[number, number]promises a pair: the minimum and the maximum. const [low, high]spreads the pair over two variables, and both have the typenumber.- The program prints
1 9.
The result annotation is needed here. Without it, TypeScript would infer the array number[] from the return, and the calling code would not know there are exactly two elements.
Interviewers like this question: how does a tuple differ from an array? An array has any length and one element type for all positions. A tuple fixes both the length and the type of each position.
An optional position
The last positions of a tuple can be made optional with ?: in [string, number?] the age may be left out. After const guest: [string, number?] = ["Борис"];, guest.length has the type 1 | 2, and guest[1] has the type number | undefined, so it must be checked before use. Optional positions come only after the required ones.
A tuple is still an array
Here the ID-card analogy ends. You cannot add a field to a printed card, but a tuple is still a JavaScript array. The call user.push(5) compiles, and the length becomes 3, although the type still speaks of two positions. You can forbid this the same way as for an array: readonly [string, number] is a read-only tuple, and it has no push.
Common mistake
A pair was put into a variable without an annotation and then passed where a tuple is expected:
function greet(user: [string, number]) {
console.log(`${user[0]}, ${user[1]}`);
}
const ann = ["Аня", 30];
greet(ann); // ✗error TS2345: Argument of type '(string | number)[]' is not assignable to parameter of type '[string, number]'.ann has the type (string | number)[]: such an array may hold any number of elements in any order, while greet expects exactly a name and an age. There are two ways to fix it:
- write an annotation on the variable:
const ann: [string, number] = ["Аня", 30];; - pass the literal straight into the call: in
greet(["Борис", 25])TypeScript reads the literal by the parameter's type and sees a tuple at once.
Key points
- A tuple is an array of a fixed length with its own type at each position:
[string, number]. - Without an annotation, TypeScript infers the literal
["Аня", 30]as the array(string | number)[]. - A read past the end of a tuple is error
TS2493, whilepushinto a tuple compiles unless it isreadonly. - The array
(string | number)[]does not fit where[string, number]is expected: it needs an annotation.