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.
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
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
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.
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.
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
function move({ x, y }: Point, [first, ...rest]: string[]) { … }
const { name, days = 5 }: Door = door;
const [head, ...tail] = names; // head: string | undefined under noUncheckedIndexedAccess
noUncheckedIndexedAccess
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;
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
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
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