TypeScript lesson 3 of 9
Arrays and Tuples
How to type a growable list of same-kind values with an array type, how to type a fixed-length, position-specific group of values with a tuple, and why the two are not interchangeable.
Published · Every example on this page was run before it was published.
An array in plain JavaScript can hold anything, in any mix, and grow or shrink freely - which also means nothing stops you from accidentally pushing the wrong kind of value into a list that was only ever supposed to hold one kind. TypeScript adds a second piece of information on top of "this is an array": what type of thing lives inside it. This lesson covers that, plus a second, closely related shape - the tuple - that looks similar but makes a very different promise.
Typed Arrays
An array type is written as the element type followed by [], or equivalently Array<ElementType> -
both mean the same thing, and you will see both in real code.
const scores: number[] = [92, 88, 79];
const names: Array<string> = ["Ana", "Ben", "Chi"];
console.log(scores);
console.log(names);[ 92, 88, 79 ]
[ 'Ana', 'Ben', 'Chi' ]Every value inside scores is checked against number, and every value inside names is checked
against string, both when the array is created and every time you add to it afterward.
Inference for Array Literals
Just as with plain variables, TypeScript can work out an array's element type from its initial values
without you writing [] anywhere:
const scores = [92, 88, 79];
console.log(scores);[ 92, 88, 79 ]scores here is inferred as number[], and that inferred type is enforced exactly as strictly as a
written one the moment you try to add something that does not belong:
const scores: number[] = [10, 20, 30];
scores.push("40");main.ts(2,13): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.push on a number[] only accepts a number, so a text value - even one that looks numeric - is
rejected before the program runs at all.
Mixed Arrays Need a Union
If a list genuinely needs to hold more than one type of value, say so explicitly with a union type
(covered in full in a later lesson) rather than reaching for any:
const mixed: (string | number)[] = ["a", 1, "b", 2];
console.log(mixed);[ 'a', 1, 'b', 2 ]The parentheses matter: (string | number)[] means "an array where every element is a string or a
number," which is different from string | number[], meaning "either a single string, or a whole array
of numbers."
Tuples: Fixed Length, Position-Specific Types
A tuple type looks similar to an array type - it also uses square brackets - but it lists a separate type for each position, and the number of positions is part of the type itself.
const point: [number, number] = [3, 4];
const entry: [string, number] = ["Ana", 92];
console.log(point);
console.log(entry);[ 3, 4 ]
[ 'Ana', 92 ]point must have exactly two elements, both numbers. entry must have exactly two elements, a string
first and then a number - swapping their order would be a type error, even though both types appear
somewhere in the tuple. Tuples are the natural type for destructuring:
const point: [number, number] = [3, 4];
const [x, y] = point;
console.log(x, y);3 4Why a Tuple Is Not Just a Short Array
Two things make a tuple stricter than an array of the same values. First, each position carries its own type, checked individually rather than as one shared element type for the whole collection:
let point: [number, number] = [3, 4];
point[0] = "3";main.ts(2,1): error TS2322: Type 'string' is not assignable to type 'number'.Second, the length itself is part of the type. A plain number[] happily accepts any number of
elements, but [number, number] promises exactly two - writing a third element into a variable of that
type is rejected by the compiler in the same way the mismatched type above was, because a [number, number, number] value simply is not a [number, number] value.
What a tuple type does not do, by default, is stop you from changing its contents after creation. A tuple is still an ordinary array underneath, so as long as a new value's type matches the position it is going into, mutating it compiles fine:
const point: [number, number] = [3, 4];
point[0] = 10;
console.log(point);[ 10, 4 ]const here only stops point itself from being rebound to a different array - it says nothing about
whether the array's contents can change, which is exactly the same rule const follows for plain
arrays and objects.
When to Reach for Each
Use an array when you have an open-ended collection of same-kind values - a list of scores, a list of names, anything where the number of items is not fixed and every item plays the same role. Use a tuple when you have a small, fixed grouping of values that are individually meaningful by position - an x/y pair, a name paired with a score, a single row of a table.
const leaderboard: [string, number][] = [
["Ana", 92],
["Ben", 88],
["Chi", 95],
];
for (const [name, score] of leaderboard) {
console.log(name, "scored", score);
}Ana scored 92
Ben scored 88
Chi scored 95leaderboard combines both ideas: it is an array (open-ended, any number of entries) of tuples (each
entry fixed at exactly a name and a score, in that order).
Common Mistakes
Choosing number[] when a fixed pair was really intended. An array type places no limit on length,
so a bug that appends a stray third value compiles without complaint even where the design only ever
meant two:
let point: number[] = [3, 4];
point.push(5);
console.log(point);
console.log(point.length);[ 3, 4, 5 ]
3Nothing here is a type error - point really is allowed to have any number of number elements,
because that is what number[] promises. If exactly two values was the intent, [number, number]
would have caught the extra push at compile time instead.
Forgetting that an un-annotated return value is inferred as an array, not a tuple, even if it looks tuple-shaped.
function makePoint() {
return [3, 4];
}
const point = makePoint();
point.push(100);
console.log(point);[ 3, 4, 100 ]Because makePoint's return type was never written down, TypeScript infers the most general type that
fits the return statement, which is number[], not [number, number]. If the fixed-pair guarantee
matters, annotate the return type explicitly: function makePoint(): [number, number] { ... }.
Reading past the end of a tuple. Because a tuple's length is part of its type, indexing past the
last declared position is caught before the program runs, unlike a plain array, which would simply hand
back undefined at runtime for an out-of-range index:
const pair: [string, number] = ["Ana", 92];
console.log(pair[2]);main.ts(2,18): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.The compiler already knows pair has exactly two positions, numbered 0 and 1, so it can rule out
index 2 without ever running the code.
Next Steps
Both binary-search and reverse-words-in-a-string lean heavily on arrays - indexing into them, slicing pieces out, and building new ones - so they are natural next stops for practicing what an array type does and does not let you do.
In the TypeScript playground, declare a tuple type for something with a natural fixed shape of your own choosing - a name and a birth year, say - and then deliberately try three things: assigning the wrong type to one position, adding a third element, and reading an out-of-range index. Compare the three error messages you get.
When you want a harder one
Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.