Patterns#

Patterns that recur in TypeScript codebases.

Strict Mode#

Turn on "strict": true in tsconfig.json. It enables a set of flags including noImplicitAny, strictNullChecks, and strictFunctionTypes that catch the majority of common bugs at compile time.

Discriminated Unions#

Model variants with a shared literal tag and switch over it. The compiler narrows each branch and warns on missing cases.

function area(s: Shape): number {
    switch (s.kind) {
        case "circle": return Math.PI * s.radius ** 2;
        case "square": return s.side ** 2;
    }
}

Branded Types#

A trick for nominally distinct types over the same underlying primitive.

type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type Email  = Brand<string, "Email">;

Avoid any#

Prefer unknown when a value’s structure isn’t known. unknown forces a narrowing check before use, where any silently disables type checking.

Schema-First Validation#

At system boundaries (HTTP, env vars, files) parse with a schema library so the rest of the program can rely on types.

import { z } from "zod";

const Config = z.object({
    port: z.number().int().positive(),
    name: z.string().min(1),
});

type Config = z.infer<typeof Config>;
const cfg: Config = Config.parse(loadEnv());

Readonly by Default#

Use readonly and ReadonlyArray<T> (or readonly T[]) to express that a value isn’t expected to change.

function sum(xs: readonly number[]): number {
    return xs.reduce((a, b) => a + b, 0);
}