Skip to main content
Software Engineering

Writing Clean, Maintainable TypeScript: Patterns I Use Daily

Practical TypeScript patterns for real codebases — discriminated unions, branded types, builder patterns, and more.

Mbeah Essilfie

Mbeah Essilfie

July 11, 2026 at 06:03 AM

9 min read 1171 views
Writing Clean, Maintainable TypeScript: Patterns I Use Daily

TypeScript Is More Than "JavaScript With Types"

Most developers use TypeScript as a type-annotation layer. But its type system is powerful enough to encode business rules, eliminate impossible states, and guide developers toward correct code.

Discriminated Unions

// Instead of optional fields that lead to impossible states:
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 exists here
      console.log(result.data.name)
      break
    case 'error':
      // TypeScript knows result.error exists here
      console.error(result.error.message)
      break
    case 'loading':
      // No data or error accessible
      break
  }
}

Branded Types

Prevent mixing up values that share the same primitive type:

type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }

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

function getPost(postId: PostId) { /* ... */ }

const userId = createUserId('abc-123')
// getPost(userId) // ❌ Type error! Can't pass UserId as PostId
Branded types add zero runtime cost. They exist purely at compile time but prevent an entire class of ID-mixup bugs.

The Builder Pattern

class QueryBuilder<T> {
  private filters: Record<string, unknown> = {}
  private sortField?: keyof T
  private limitValue?: number

  where<K extends keyof T>(field: K, value: T[K]): this {
    this.filters[field as string] = value
    return this
  }

  orderBy(field: keyof T): this {
    this.sortField = field
    return this
  }

  limit(n: number): this {
    this.limitValue = n
    return this
  }

  build() {
    return { filters: this.filters, sort: this.sortField, limit: this.limitValue }
  }
}

// Usage - fully typed!
const query = new QueryBuilder<Post>()
  .where('status', 'published')
  .orderBy('publishedAt')
  .limit(10)
  .build()

Exhaustive Checks

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

type Theme = 'light' | 'dark' | 'system'

function getBackground(theme: Theme): string {
  switch (theme) {
    case 'light': return '#ffffff'
    case 'dark': return '#1a1a1a'
    case 'system': return 'var(--bg)'
    default: return assertNever(theme) // Compile error if you miss a case
  }
}

Summary

Good TypeScript makes invalid states unrepresentable. Use the type system as documentation that the compiler enforces.

TypeScriptArchitecture
Mbeah Essilfie

Written by Mbeah Essilfie

Fullstack Software Developer

Read next

The Complete Guide to Vue 3 Composables
12 min read

The Complete Guide to Vue 3 Composables

Understand the power of Vue 3's Composition API through practical, real-world composable patterns that you can use today.

Mbeah EssilfieMbeah Essilfie
981