Arrays, tuples and as const

Say how many there are and whether they can change — and freeze a literal into a type.

08of 14
≈ 11Min read
8Parts

Arrays and readonly arrays

readonly Door[] removes every mutating method. It is the honest type for a parameter you are not going to change, and it documents that at the signature rather than in a comment.

two levels of "do not touch"
const names: string[] = ["a"];
const frozen: readonly string[] = ["a"];

frozen.push("b");        // Error: push does not exist on a readonly array
frozen.concat("b");      // fine — it returns a new array

interface Props { readonly items: readonly Door[] }

Tuples

fixed length, and each position has its own type
type Point = [number, number];
type Entry = [key: string, value: number];        // names are documentation
type Range = [start: number, end?: number];       // optional at the end
type Path = [string, ...string[]];                 // at least one

const [x, y]: Point = [10, 20];
const state: [Door[], (d: Door) => void] = useState();   // the React pattern
An array literal is not a tuple

const p = [10, 20] is number[], so it is not assignable to [number, number]. Annotate it, or use as const.

as const

as const says "this literal is exactly what it looks like, and it will not change". Everything becomes readonly and every value keeps its literal type — which is what turns a list into a union.

the most useful two words in the language
const finishes = ["painted", "oiled"];             // string[]
const finishes = ["painted", "oiled"] as const;     // readonly ["painted", "oiled"]

type Finish = typeof finishes[number];              // "painted" | "oiled"

const config = { retries: 3, mode: "strict" } as const;
config.retries = 4;                                 // Error: read-only

satisfies

satisfies is the answer to "I want it checked against a type, but I want to keep the specific one". Reach for it whenever an annotation would throw away information you wanted.

check the shape without widening the type
type Config = Record<string, string | number>;

const config = { url: "/api", retries: 3 } satisfies Config;

config.url.toUpperCase();      // still known to be a string
// with ": Config" it would have been string | number and this would not compile

Destructuring with types

annotate the whole pattern, not the pieces
function move({ x, y }: Point, [first, ...rest]: string[]) { … }

const { name, days = 5 }: Door = door;
const [head, ...tail] = names;          // head: string | undefined under noUncheckedIndexedAccess

noUncheckedIndexedAccess

the flag that tells the truth about indexing
const doors: Door[] = [];
const first = doors[0];        // Door          — without the flag, and it is a lie
const first = doors[0];        // Door | undefined — with it

const first = doors.at(0);     // Door | undefined either way
if (doors[0]) { … }
const [first] = doors;
It is noisy and it is right

Every arr[i] really can be undefined. Turn it on in a new project; in an old one, expect an afternoon of ?? fallback.

Typing what a map and a filter return

filter does not narrow — a predicate does
const maybe: (Door | undefined)[] = …;

const doors = maybe.filter(Boolean);           // still (Door | undefined)[]
const doors = maybe.filter((d) => d !== undefined);   // TS 5.5+ narrows this correctly
const doors = maybe.filter((d): d is Door => d !== undefined);   // explicit, works everywhere

Arrays of unions versus unions of arrays

they are different types
type A = (string | number)[];     // each item is one or the other
type B = string[] | number[];     // the whole array is one or the other

const a: A = ["x", 1];            // fine
const b: B = ["x", 1];            // Error

Practice