DEV Community

Moonlit Capy
Moonlit Capy

Posted on • Edited on • Originally published at blog.elunari.uk

TypeScript Patterns Every Developer Should Know in 2026

TypeScript has matured far beyond "JavaScript with types." The type system is powerful enough to encode complex business logic at compile time. Here are the patterns that make the biggest difference in production codebases.

Discriminated Unions

Model states where different variants carry different data. TypeScript narrows the type automatically based on the discriminant.

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

function renderState<T>(state: AsyncState<T>) {
  switch (state.status) {
    case "idle": return "Ready"
    case "loading": return "Loading..."
    case "success": return `Data: ${JSON.stringify(state.data)}`
    case "error": return `Error: ${state.error.message}`
  }
}
Enter fullscreen mode Exit fullscreen mode

This is the foundation of type-safe state management. Every React app with async data should use this pattern instead of separate isLoading, isError, data booleans.

Branded Types

Prevent mixing up values that are the same primitive type but represent different things.

type UserId = string & { readonly __brand: "UserId" }
type OrderId = string & { readonly __brand: "OrderId" }

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

function getOrder(orderId: OrderId) { /* ... */ }

const userId = createUserId("user_123")
// getOrder(userId) // Compile error! Can't pass UserId where OrderId expected
Enter fullscreen mode Exit fullscreen mode

Zero runtime cost. The brand exists only in the type system. Use this for any ID type, currency amount, or validated string.

Template Literal Types

Generate string unions from combinations:

type EventName = `${"user" | "order"}.${"created" | "updated" | "deleted"}`
// Result: "user.created" | "user.updated" | "user.deleted" | "order.created" | "order.updated" | "order.deleted"

type CSSProperty = `${string}-${string}`
type HexColor = `#${string}`
Enter fullscreen mode Exit fullscreen mode

This is powerful for event systems, CSS-in-JS, and any API where string patterns matter.

Result Types for Error Handling

Stop throwing exceptions. Return typed results instead:

type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E }

function parseConfig(input: string): Result<Config, ParseError> {
  try {
    const config = JSON.parse(input)
    return { ok: true, value: config }
  } catch (e) {
    return { ok: false, error: new ParseError(e.message) }
  }
}

const result = parseConfig(input)
if (result.ok) {
  console.log(result.value) // TypeScript knows this is Config
} else {
  console.error(result.error) // TypeScript knows this is ParseError
}
Enter fullscreen mode Exit fullscreen mode

The caller is forced to handle both cases. No more uncaught exceptions in production.

Exhaustive Matching

Use the never type to ensure you handle every case in a union:

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

function handleStatus(status: AsyncState<unknown>) {
  switch (status.status) {
    case "idle": return "Ready"
    case "loading": return "Loading..."
    case "success": return "Done"
    case "error": return "Failed"
    default: return assertNever(status) // Compile error if a case is missing
  }
}
Enter fullscreen mode Exit fullscreen mode

When you add a new variant to the union, TypeScript will flag every switch statement that doesn't handle it.

Wrapping Up

Start with discriminated unions and exhaustive matching — they change how you think about state management. Layer in branded types and Result types as your codebase grows. The goal: make illegal states unrepresentable.

These patterns have zero runtime overhead. They exist purely in the type system, disappearing completely after compilation. That's the beauty of TypeScript done right.


Want more dev content? Check out the blog or buy me a coffee.


Keep Leveling Up

If you found this useful, check out my Dev Reference Card - a downloadable cheat sheet with essential developer patterns, shortcuts, and references condensed into one page.

Grab it here for the price of a coffee.

Browse all my tools and templates at moonlitcapy.gumroad.com

Top comments (2)

Collapse
 
pmoati profile image
Pierre Moati

Great patterns.

One thing I'd add: these become even more powerful when they compose.
For example, combining Result types with schema validation lets you validate input, get a typed Result back, and chain transformations :
No try/catch, full type inference.

That's the pattern I use most in production ! ;)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.