Skip to Content
Grab N GoTS Patterns

TS Patterns

Targeting TypeScript 5.x

Syntax

Utility Types
interface User { id: number name: string email: string } Partial<User> // all optional Required<User> // all required Pick<User, 'id' | 'name'> // subset Omit<User, 'email'> // exclude Readonly<User> // immutable Record<string, number> // { [key]: num }
Extract & Exclude
type Status = 'ok' | 'error' | 'loading' | 'idle' // keep only members assignable to union type Active = Extract<Status, 'ok' | 'loading'> // 'ok' | 'loading' // remove members assignable to union type Inactive = Exclude<Status, 'ok' | 'loading'> // 'error' | 'idle' // works with object types too type Fn = Extract<string | (() => void), Function> // () => void
Overloads
function format(val: string): string function format(val: number): string function format(val: string | number): string { if (typeof val === 'string') return val.trim() return val.toFixed(2) } format('hello') // string signature format(3.14159) // number signature

Beginner Patterns

Discriminated Union
type Result = | { status: 'ok'; data: string } | { status: 'error'; message: string } function handle(r: Result) { switch (r.status) { case 'ok': r.data // TS knows data exists break case 'error': r.message // TS knows message exists break } }
Type Guards
interface Dog { bark(): void } interface Cat { meow(): void } // custom guard — returns `is` type function isDog(pet: Dog | Cat): pet is Dog { return 'bark' in pet } const pet: Dog | Cat = getPet() if (isDog(pet)) { pet.bark() // TS knows it's Dog }
as const
const routes = ['/', '/about', '/blog'] as const // readonly ['/', '/about', '/blog'] type Route = (typeof routes)[number] // '/' | '/about' | '/blog' const config = { env: 'prod', port: 3500 } as const // { readonly env: 'prod'; readonly port: 3500 }
Satisfies
type Colors = Record<string, string | string[]> // validates type without widening const palette = { red: '#ff0000', blue: ['#0000ff', '#0033cc'], } satisfies Colors // TS still knows red is string, blue is string[] palette.red.toUpperCase() // works palette.blue.map(c => c) // works

Intermediate Patterns

Branded / Opaque Types
// prevent mixing structurally identical types type USD = number & { __brand: 'USD' } type EUR = number & { __brand: 'EUR' } function usd(amount: number): USD { return amount as USD } function eur(amount: number): EUR { return amount as EUR } function charge(amount: USD) { /* ... */ } charge(usd(10)) // ok charge(10) // Error: number is not USD charge(eur(10)) // Error: EUR is not USD
Exhaustive Checks
type Shape = | { kind: 'circle'; radius: number } | { kind: 'square'; side: number } function area(s: Shape): number { switch (s.kind) { case 'circle': return Math.PI * s.radius ** 2 case 'square': return s.side ** 2 default: // if Shape grows, this errors at compile time const _exhaustive: never = s return _exhaustive } }
Declaration Merging
// interfaces with the same name merge interface Config { port: number } interface Config { host: string } // Config = { port: number; host: string } // augmenting third-party modules declare module 'express' { interface Request { userId?: string } }
Module Augmentation
// extend an existing module's types // e.g., add custom properties to Window declare global { interface Window { analytics: { track(event: string): void } } } window.analytics.track('click') // now typed export {} // makes this file a module

Advanced Patterns

Builder Pattern Types
class QueryBuilder<T extends object = {}> { private filters: Record<string, unknown> = {} where<K extends string, V>( key: K, value: V ): QueryBuilder<T & Record<K, V>> { this.filters[key] = value return this as any } build(): T { return this.filters as T } } // type accumulates with each call const q = new QueryBuilder() .where('name', 'Tammy') // { name: string } .where('age', 28) // & { age: number } .build() // q: { name: string; age: number }
Readonly Deep
// recursively make every property readonly type DeepReadonly<T> = T extends Function ? T : T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } : T interface Config { db: { host: string; port: number } debug: boolean } const config: DeepReadonly<Config> = { db: { host: 'localhost', port: 5432 }, debug: true, } config.db.port = 3500 // Error: readonly
Last updated on