Lesson 9 of 25
Arrays, Tuples, and Enums
readonly arrays, fixed-length tuples with a type per position, and enums versus the union-of-literals alternative most style guides now prefer.
Arrays cover "many of the same thing." This lesson covers two more specific shapes: a fixed-length, mixed-type tuple, and a fixed set of named constants — plus the modern alternative most codebases now prefer for the latter.
readonly arrays
readonly T[] (or, equivalently, ReadonlyArray<T>) removes every mutating
method from the type — .push(), .pop(), .splice(), and index
assignment are all compile errors, while .map(), .filter(), and
.slice() (which return a new array) remain available:
function printAll(items: readonly string[]) {
items.push("nope");
// Property 'push' does not exist on type 'readonly string[]'.
console.log(items.join(", ")); // fine — this doesn't mutate
}This is genuinely useful as a signal in a function signature: readonly string[] tells a caller (and the compiler) "this function will not modify
your array," which a plain string[] parameter can't promise.
Tuples: a fixed shape, not a fixed content
A tuple is an array with a known, fixed length, where each position can have its own type:
let point: [number, number] = [3, 4];
let entry: [string, number] = ["age", 30];
point = [3, 4, 5];
// Source has 3 element(s) but target allows only 2.This is different from number[], which allows any length but requires
every element to be the same type. useState in React, Object.entries(),
and coordinate pairs are all common real-world tuples.
Optional and rest elements work in tuples too:
type Range = [start: number, end?: number]; // end is optional
type Args = [command: string, ...flags: string[]]; // rest of any lengthThe names (start, end, command) here are purely for readability in
tooltips and error messages — they don't change what's assignable, the same
way parameter names don't affect a function's callability.
Enums
An enum names a fixed set of related constants:
enum Status {
Pending,
Done,
Failed,
}
let current: Status = Status.Pending;By default, members are numbered from 0. You can assign your own values,
including strings:
enum Direction {
Left = "LEFT",
Right = "RIGHT",
}
let dir: Direction = Direction.Left; // "LEFT"The important thing to understand — and the reason enums get a full section here rather than a passing mention — is that an enum is not erased the way every other type in this track is. It compiles to a real JavaScript object at runtime:
// What "enum Status { Pending, Done }" actually compiles to, roughly:
var Status;
(function (Status) {
Status[(Status["Pending"] = 0)] = "Pending";
Status[(Status["Done"] = 1)] = "Done";
})(Status || (Status = {}));That reverse mapping (Status[0] === "Pending") only exists for numeric
enums, not string enums, which is one of a handful of inconsistencies that
make enums slightly harder to reason about than most of the type system.
The literal-union alternative
For a fixed set of string options, a union of string literals is usually the better default in modern TypeScript:
type Status = "pending" | "done" | "failed";
function markDone(status: Status) {
/* ... */
}
markDone("done"); // fine
markDone("finished");
// Argument of type '"finished"' is not assignable to type 'Status'.Compared to an enum, this:
- Compiles to nothing at runtime — it's erased like every other type, matching the "types cost nothing" rule from the very first lesson.
- Matches plain strings for free. JSON from an API, form values, and CSV
data are all plain strings — a literal union just works with them, while
an enum needs an explicit
Status.Pendingreference or a cast. - Avoids the numeric-enum default entirely.
Status.Pendingbeing0by default means a caller passing the raw number0where aStatuswas expected type-checks by accident, which a string-literal union doesn't allow.
Enums are still common in existing codebases and some tooling (and const enum, a variant erased at compile time in exchange for some restrictions,
addresses the runtime-cost objection specifically) — but for new code, reach
for a literal union first, and an enum only if you have a specific reason
to.
Try it yourself
What to remember
- A tuple (
[number, string]) fixes both length and per-position type; a plain array (number[]) fixes only the element type, at any length. readonly T[]removes mutating array methods at compile time — nothing changes at runtime.- A regular
enumis not erased — it produces a real JavaScript object, unlike every other type construct in this track. - A union of string literals is usually the better default for a fixed set of string options: zero runtime cost, and it matches plain strings from JSON without conversion.
Check yourself
4 questions · pass 3/4 to unlock unknown, any, and Type Guards
1.What is the type of
const point: [number, number] = [3, 4];?2.What does marking an array
readonly number[]prevent?3.What does a numeric enum like
enum Status { Pending, Done }actually compile to?4.Given
type Status = "pending" | "done" | "failed";, what is a common reason to prefer this over an enum for a fixed set of string options?
4 left to answer