All articles
TypeScript12 min read

TypeScript Patterns for Large-Scale React Apps

Practical TypeScript patterns I've used to keep codebases maintainable at scale — from discriminated unions to branded types and exhaustive checks.

2024-01-15


After years of working on large React codebases at Carestack and Hashedin, I've settled on a handful of TypeScript patterns that genuinely improve maintainability. These aren't academic exercises — they solve real problems.

Discriminated Unions for State

Instead of booleans and nullable fields that can get into impossible states, use discriminated unions:

type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

Now your component can't accidentally access data when status === "error". TypeScript catches it at compile time.

Branded Types

Branded (or opaque) types prevent mixing up values of the same primitive type:

type UserId = string & { readonly _brand: "UserId" };
type PostId = string & { readonly _brand: "PostId" };

function getUser(id: UserId) { ... }

const postId = "abc" as PostId;
getUser(postId); // TS error — correct!

Use a helper function to create branded values and keep the casting in one place.

Exhaustive Checks

When you switch on a union, ensure every case is handled:

function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`);
}

switch (state.status) {
  case "idle": return <Idle />;
  case "loading": return <Spinner />;
  case "success": return <Data data={state.data} />;
  case "error": return <Error message={state.error} />;
  default: return assertNever(state);
}

If you add a new variant to RequestState and forget to handle it, TypeScript errors immediately.

Satisfies Operator

The satisfies operator (TS 4.9+) lets you validate a value against a type while keeping its inferred literal type:

const config = {
  theme: "dark",
  lang: "en",
} satisfies Record<string, string>;

// config.theme is "dark", not just string

Conclusion

These patterns pay for themselves quickly. The key is consistency — pick a set and apply them uniformly across the codebase. TypeScript's value compounds when you use it deliberately.