AniUI Academy

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.

10 min read

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 length

The 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.Pending reference or a cast.
  • Avoids the numeric-enum default entirely. Status.Pending being 0 by default means a caller passing the raw number 0 where a Status was 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

Try it yourself
Loading playground...

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 enum is 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

up to 50
  1. 1.What is the type of const point: [number, number] = [3, 4];?

  2. 2.What does marking an array readonly number[] prevent?

  3. 3.What does a numeric enum like enum Status { Pending, Done } actually compile to?

  4. 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