April 10, 2026·9 min·By

Advanced TypeScript Patterns That Actually Solve Real Problems

TypeScriptadvanced TypeScripttype systempatterns

Most TypeScript codebases use about 20% of what the type system can do. These patterns solve real problems you've probably worked around with any or as.

Discriminated Unions

Model state machines and API responses with perfect type narrowing:

type ApiResult<T> =
    | { status: 'success'; data: T }
    | { status: 'error'; error: string; code: number }
    | { status: 'loading' }

function handleResult<T>(result: ApiResult<T>) {
    switch (result.status) {
        case 'success':
            console.log(result.data)  // TypeScript knows data exists
            break
        case 'error':
            console.log(result.error, result.code)  // TypeScript knows error/code exist
            break
        case 'loading':
            // result.data doesn't exist here -- TypeScript enforces this
            break
    }
}

Template Literal Types

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
type ApiRoute = `/api/${string}`
type Endpoint = `${HttpMethod} ${ApiRoute}`

// Valid: "GET /api/users", "POST /api/orders"
// Invalid: "FETCH /api/users" -- TypeScript error

type CSSVariable = `--${string}`
type CssColor = `#${string}` | `rgb(${string})` | `rgba(${string})`

// Extract keys and transform them
type EventNames = 'click' | 'focus' | 'blur'
type EventHandlers = {
    [K in EventNames as `on${Capitalize<K>}`]: () => void
}
// Results in: { onClick: () => void; onFocus: () => void; onBlur: () => void }

Conditional Types

// Extract the element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never

type StringArray = string[]
type Str = ElementOf<StringArray>  // string

// Make specific keys required
type RequireKeys<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

interface Config {
    apiUrl?: string
    timeout?: number
    retries?: number
}

type ValidConfig = RequireKeys<Config, 'apiUrl'>
// { apiUrl: string; timeout?: number; retries?: number }

// Flatten promise types
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T
type Result = Awaited<Promise<Promise<string>>>  // string

Branded Types

Prevent mixing up IDs or units at the type level:

declare const __brand: unique symbol

type Brand<T, B> = T & { [__brand]: B }

type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
type Dollars = Brand<number, 'Dollars'>
type Cents = Brand<number, 'Cents'>

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

function getUser(id: UserId): Promise<User> {
    return db.users.findById(id)
}

const orderId = makeOrderId('ord_123')
getUser(orderId)  // TypeScript error! OrderId is not assignable to UserId

The Builder Pattern for Type-Safe Query Construction

class QueryBuilder<T extends object, Selected extends keyof T = keyof T> {
    private query: Partial<T> = {}
    private selectedFields: (keyof T)[] = []

    where<K extends keyof T>(key: K, value: T[K]): this {
        this.query[key] = value
        return this
    }

    select<K extends keyof T>(...fields: K[]): QueryBuilder<T, K> {
        this.selectedFields = fields
        return this as any
    }

    build(): Pick<T, Selected>[] {
        return [] as Pick<T, Selected>[]  // Implementation would query DB
    }
}

interface User {
    id: string
    email: string
    name: string
    createdAt: Date
}

const users = new QueryBuilder<User>()
    .where('email', 'user@example.com')
    .select('id', 'email')
    .build()

// users is Pick<User, 'id' | 'email'>[] -- TypeScript knows the shape
users[0].id     // OK
users[0].name   // TypeScript error! 'name' was not selected

Satisfies Operator

type ColorMap = Record<string, string>

// Old way: loses the literal types
const colors: ColorMap = {
    primary: '#FCE38A',
    secondary: '#0A0A0A'
}
colors.primary  // type: string (not '#FCE38A')

// With satisfies: validates the type but keeps literal types
const colors2 = {
    primary: '#FCE38A',
    secondary: '#0A0A0A'
} satisfies ColorMap

colors2.primary  // type: '#FCE38A' (literal preserved)
colors2.invalid  // TypeScript error! (type check still runs)

Exhaustive Checks

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

type Shape = 'circle' | 'square' | 'triangle'

function getArea(shape: Shape, size: number): number {
    switch (shape) {
        case 'circle': return Math.PI * size ** 2
        case 'square': return size ** 2
        case 'triangle': return (size ** 2) / 2
        default: return assertNever(shape)  // Compile error if you add a new Shape and forget to handle it
    }
}

When you add 'pentagon' to Shape, TypeScript immediately flags the switch statement as incomplete. No more runtime bugs from unhandled cases.

These patterns make TypeScript earn its keep. They're not just clever tricks -- they prevent entire categories of bugs at compile time, which is where bugs are cheapest to fix.

K
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

Does Innovibe build this kind of thing for clients?+

Yes — this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

How do I decide whether to build this in-house or hire an agency?+

Build in-house if your team has the skills and bandwidth and this is core to your product. Hire out if it's infrastructure, if speed matters, or if the expertise gap would take months to close. We're biased, obviously — but we'll tell you honestly when in-house makes more sense.

What tech stack does Innovibe use for projects like this?+

Next.js + TypeScript on the frontend, Node.js or Go on the backend, Postgres for the primary data store, and GCP (Cloud Run, BigQuery, Pub/Sub) for infrastructure. We pick tools that are boring in the best way — proven, well-documented, and easy to hire for.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation