← Back to logs

TypeScript Patterns I Actually Use

·2 min read·
TypeScriptJavaScript

Not the advanced wizardry — just the practical patterns that show up in real codebases and make the type system work for you.

There's a gap between "TypeScript exists" and "TypeScript is making my codebase meaningfully safer." These are the patterns that close that gap in day-to-day work.

Discriminated Unions for State

Instead of a mess of optional fields, model mutually exclusive states explicitly:

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

Now exhaustive switch statements work and TypeScript narrows data and error to the right variants automatically.

satisfies for Config Objects

satisfies validates against a type without widening — you keep the literal types:

const routes = {
  home: "/",
  blog: "/blog",
  contact: "/contact",
} satisfies Record<string, string>;

// routes.home is typed as "/" not string

This is great for config objects where you want both validation and autocompletion on the literal values.

Awaited<ReturnType<...>>

When you need the resolved return type of an async function without duplicating a type definition:

async function getUser(id: string) {
  return db.user.findUnique({ where: { id } });
}

type User = Awaited<ReturnType<typeof getUser>>;

Branded Types for IDs

Plain string types for IDs are too permissive. Brand them to prevent mixing up UserId and PostId:

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

function createUserId(id: string): UserId {
  return id as UserId;
}

Now passing a PostId where a UserId is expected is a type error.

infer for Extracting Types

Useful when you need to pull a type out of a generic:

type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type UnpackArray<T> = T extends Array<infer U> ? U : T;

Closing Thoughts

The goal isn't type gymnastics — it's catching real bugs. Discriminated unions and branded types give you the most safety per line of type code. Use the rest when the problem actually calls for it.