Back to blog

TypeScript Patterns I Use Every Day

Practical TypeScript patterns for building robust applications — from discriminated unions to branded types and const assertions.

2 min read
TypeScriptPatterns

Beyond Basic Types

TypeScript is more than just adding : string to your variables. The type system is expressive enough to encode business logic, prevent entire categories of bugs, and make refactoring fearless.

Here are the patterns I reach for most often.

Discriminated Unions

When you have a value that can be in one of several states, discriminated unions are the way to go:

type Result<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: Error }
  | { status: "loading" };

function handleResult(result: Result<User>) {
  switch (result.status) {
    case "success":
      // TypeScript knows result.data is User here
      console.log(result.data.name);
      break;
    case "error":
      // TypeScript knows result.error is Error here
      console.error(result.error.message);
      break;
    case "loading":
      // No other properties available
      break;
  }
}

The compiler guarantees exhaustive handling — if you add a new status, every switch statement will produce a type error until you handle it.

Const Assertions

Use as const to narrow literal types and make objects deeply readonly:

const ROUTES = {
  home: "/",
  blog: "/blog",
  projects: "/projects",
} as const;

// Type is "/" | "/blog" | "/projects" — not just string
type Route = (typeof ROUTES)[keyof typeof ROUTES];

Template Literal Types

One of the most powerful features for string manipulation at the type level:

type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

Wrapping Up

These patterns have saved me countless hours of debugging. The upfront investment in precise types pays dividends in code quality and developer confidence.