Arrays and Tuples
Ordered collections of a single type, fixed-length tuples where position carries meaning, and the index access that types promise but do not check.
Ordered collections of a single type, fixed-length tuples where position carries meaning, and the index access that types promise but do not check.
You have four hundred photographs to process and everything so far handles exactly one thing at a time. You could declare four hundred names. Nobody has ever done that twice.
What you need is one name for many values, which you can add to, work through and pass around as a single thing. By the end of this lesson you will have that, know how the checker keeps track of what is inside, and have met the position where absence sneaks back in without a warning.
const photos = ["dawn.jpg", "tram.jpg", "square.jpg"];
console.log(photos.length); // 3The type is string[] — "an array of strings" — and TypeScript
worked that out from the contents. You can write it:
const photos: string[] = ["dawn.jpg", "tram.jpg"];
const sizes: Array<number> = [340, 128]; // the same thingstring[] is the common form. Both mean an array whose elements
are all that type.
An empty array is the one place you must annotate:
const processed = []; // never[] - nothing can go in it
const processed: string[] = []; // this is what you meantWith nothing to look at, the checker infers an array that can
hold nothing, and the first push is an error. Annotate empty
arrays.
Positions start at zero:
photos[0]; // "dawn.jpg"
photos[photos.length - 1]; // the last one
photos.at(-1); // the last one, more directly.at(-1) counts from the end and is much clearer than the
arithmetic.
Taking a range:
photos.slice(0, 2); // the first two - up to but NOT including 2
photos.slice(2); // from position 2 to the end
photos.slice(-2); // the last twoslice always returns a new array and leaves the original
alone. Its similarly-named neighbour splice changes the array
in place, which is a genuinely unfortunate pair of names —
reaching for the wrong one silently mutates something you meant
to copy.
Now the thing to know about arrays and the checker.
const photos: string[] = [];
const first = photos[0]; // typed as string
first.toUpperCase(); // compiles - crashes at runtimeThe array is empty, so photos[0] is undefined — but the
declared type says string[], so the checker believes every
position holds a string. This is a deliberate unsoundness:
requiring a check on every index would be unbearable in
practice.
One option fixes it:
{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}Now photos[0] is string | undefined and you must handle it —
which is correct, and which is why the null lesson recommended
this alongside strict.
Without that option, the safe habits are .at() with a check,
or the methods in the next section, which are honest about
finding nothing.
Rather than looping by position, four methods cover most of what you will do. Each takes a function and applies it to every element.
const sizes = [340, 128, 8000, 95];
sizes.map((size) => size * 2); // [680, 256, 16000, 190]
sizes.filter((size) => size > 200); // [340, 8000]
sizes.find((size) => size > 200); // 340, or undefined
sizes.reduce((total, size) => total + size, 0); // 8563map — same length, different contents
Transforms every element. Four in, four out.
filter — same contents, shorter
Keeps the ones that pass a test. Nothing is transformed.
find — one element, or undefined
The first match. The signature says | undefined, which is
absence arriving honestly for once.
reduce — one value of any shape
Combines everything, starting from the value you pass as the second argument.
The types flow through, which is the part worth noticing:
You annotated nothing and the checker knows both. Get one wrong and it says so.
A few more that come up constantly:
The ... is the spread, and it is how you build a new array
from an existing one rather than modifying it.
Arrays are mutable, unlike strings. Some methods change them in place and return nothing useful:
sort is the one that catches people twice.
Bad — sorting numbers, and sorting the original.
Good — a comparison function, on a copy.
Two independent failures in one line.
8000 comes before 95.
With no argument, sort converts everything to a string
first, and "8" comes before "9".
Always pass a comparator for anything that is not text.
sizes changed too.
The line reads like it produced something new, and it modified what it was given.
Copy first with [...sizes], or use toSorted(), which
returns a new array.
Always pass a comparator for anything that is not text, and
copy first unless you deliberately want to mutate. Newer
toSorted() and toReversed() return a new array and avoid the
second half of this entirely.
The distinction the values lesson promised:
const protects the binding, not the contents. For contents,
say so in the type:
readonly string[] allows reading, map, filter and slice,
and removes the mutating methods. It is a good default for
function parameters — a function that only reads a list should
say so, and then it cannot surprise its caller by modifying the
array they passed in.
A tuple is an array with a fixed length where each position means something different:
Contrast with number[], which is any quantity of numbers, all
meaning the same kind of thing. [number, number] is exactly
two, in a known order — a width then a height.
The checker enforces both the length and the position types:
Names make them readable:
The labels are for humans and tooling; they do not change the type.
The rule for choosing: if the positions mean different things, it is a tuple. If every position is the same kind of thing and there could be more or fewer, it is an array.
You can now hold many values under one name, transform and
filter them with the types flowing through automatically, and
you know the two array traps that produce wrong answers rather
than errors — a default sort on numbers, and an index the
checker trusts more than it should.
Next is Objects and Type Aliases, the other way to group data. An array holds many things of one kind; an object holds a few named things of different kinds, which is how you describe one customer, one photo, one order.
Before you move on, take an array of numbers and sort it without
a comparator. Look at the result and work out why 8000 came
before 95. Then check whether the original array changed.
Producing both mistakes deliberately, once, is what makes you
notice them in someone else's code.
DECLARING
const photos = ["a.jpg"]; inferred as string[]
const sizes: number[] = []; ANNOTATE empty arrays
(otherwise never[])
GETTING OUT
photos[0] photos.at(-1) at() counts from the end
photos.slice(0, 2) a NEW array, up to but not
including 2
photos.length
photos[0] is typed as present even when it is not
turn on noUncheckedIndexedAccess to make it T | undefined
WORKING THROUGH - all return something new
map(fn) transform every element
filter(fn) keep the ones that pass
find(fn) the first match, or UNDEFINED
reduce(fn, x) combine into one value
some/every is at least one / are they all
includes(v) yes/no indexOf(v) position, or -1
join(", ") -> one string
[...a, x] a new array with one more
[...a, ...b] two joined
CHANGING IN PLACE
push pop shift unshift splice sort reverse
toSorted() toReversed() the copying versions
sort() with no argument sorts as TEXT: 8000 before 95
sort() mutates the original
-> [...a].sort((x, y) => x - y)
CONST AND READONLY
const protects the NAME, not the contents
readonly string[] no push, no sort - good for parameters
TUPLES
[number, number] fixed length, positions mean different things
[x: number, y: number] labels, for readers
number[] any length, all the same kind of thing
DESTRUCTURING
const [a, b, ...rest] = photos;
const [, , third] = photos;const names = photos.map((p) => p.name); // string[]
const lengths = names.map((n) => n.length); // number[]sizes.some((s) => s > 5000); // is at least one?
sizes.every((s) => s > 0); // are they all?
sizes.includes(128); // is this value present?
sizes.indexOf(128); // where? -1 if absent
photos.join(", "); // -> a single string
[...photos, "new.jpg"]; // a NEW array with one more
[...photos, ...others]; // two arrays joinedphotos.push("bridge.jpg"); // add to the end
photos.pop(); // remove and return the last
photos.shift(); // remove and return the first
photos.unshift("first.jpg"); // add at the front
photos.sort(); // sorts IN PLACE
photos.reverse(); // reverses IN PLACE
photos.splice(1, 2); // removes 2 elements from position 1const sizes = [340, 128, 8000, 95];
const sorted = sizes.sort();
console.log(sorted); // [128, 340, 8000, 95]
console.log(sizes); // [128, 340, 8000, 95] - changed tooconst sizes = [340, 128, 8000, 95];
const sorted = [...sizes].sort((a, b) => a - b);
console.log(sorted); // [95, 128, 340, 8000]
console.log(sizes); // [340, 128, 8000, 95] - untouchedconst photos = ["dawn.jpg"];
photos.push("tram.jpg"); // fine - the name still points here
photos = []; // error - repointing the nameconst photos: readonly string[] = ["dawn.jpg"];
photos.push("tram.jpg");
// Property 'push' does not exist on type 'readonly string[]'.const dimensions: [number, number] = [1920, 1080];
const entry: [string, number] = ["dawn.jpg", 340];
const [width, height] = dimensions; // destructuringdimensions[2]; // error - length is 2
dimensions[0] = "wide"; // error - position 0 is a numberconst point: [x: number, y: number] = [10, 20];const [first, second, ...rest] = photos;
const [, , third] = photos; // skip the first two