正在加载,请稍候…

TypeScript Advanced Patterns: Conditional Types, Template Literals, and Branded Types

Master advanced TypeScript patterns including conditional types, template literal types, branded types, type guards, mapped types, and the infer keyword for production-grade type safety.

Beyond Basic TypeScript

Most TypeScript guides cover interfaces, generics, and union types. This guide covers features that separate a TypeScript expert from a beginner: the type-level programming constructs that let you express complex invariants at compile time, eliminating entire categories of runtime bugs.

Conditional Types

Conditional types let the TypeScript type system make decisions:

// Basic form: T extends U ? TrueType : FalseType
type IsString<T> = T extends string ? true : false

type A = IsString<string>  // true
type B = IsString<number>  // false

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

type StrElem = ElementType<string[]>  // string
type NumElem = ElementType<number[]>  // number
type NotArray = ElementType<string>   // never

Distributive Conditional Types

When you apply a conditional type to a union, it distributes over each member:

type ToArray<T> = T extends any ? T[] : never

// Distributes: (string extends any ? string[] : never) | (number extends any ? number[] : never)
type Result = ToArray<string | number>  // string[] | number[]

// Prevent distribution by wrapping in a tuple:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Combined = ToArrayNonDist<string | number>  // (string | number)[]

The infer Keyword

infer captures a type within a conditional type:

// Extract return type (TypeScript has ReturnType<T> built-in)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

// Extract what a Promise resolves to
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

type UserData = Awaited<Promise<{ id: string; name: string }>>
// { id: string; name: string }

// Extract first parameter
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never
type EmailParam = FirstParam<(email: string, verified: boolean) => void>  // string

// Deep extraction from nested structures
type UnpackNested<T> = T extends { data: { items: (infer I)[] } } ? I : never
type Item = UnpackNested<{ data: { items: User[] } }>  // User

Template Literal Types

Template literal types generate string types programmatically:

// Basic combination
type Greeting = `Hello, ${string}!`
const g1: Greeting = 'Hello, World!'  // OK
const g2: Greeting = 'Hi, World!'     // Error

// Combining unions — cartesian product
type Color  = 'red' | 'green' | 'blue'
type Shade  = 'light' | 'dark'
type ColorVariant = `${Shade}-${Color}`
// "light-red" | "light-green" | ... | "dark-blue"

// CSS property names
type CSSProperty  = 'margin' | 'padding' | 'border'
type CSSDirection = 'top' | 'right' | 'bottom' | 'left'
type DirectionalCSS = `${CSSProperty}-${CSSDirection}`

Type-Safe Event System

type EventName   = 'user' | 'post' | 'comment'
type EventAction = 'created' | 'updated' | 'deleted'
type AppEvent    = `${EventName}:${EventAction}`

class TypedEmitter {
  on(event: AppEvent, handler: (data: unknown) => void): void { ... }
  emit(event: AppEvent, data: unknown): void { ... }
}

const emitter = new TypedEmitter()
emitter.on('user:created', handler)  // OK
emitter.on('user:removed', handler)  // Error: 'removed' not in EventAction

String Manipulation Types

type DataKeys = 'firstName' | 'lastName' | 'email'
type GetterNames = `get${Capitalize<DataKeys>}`
// "getFirstName" | "getLastName" | "getEmail"

// Derived getter type from data object
type Getters<T extends Record<string, unknown>> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

type UserGetters = Getters<{ name: string; age: number }>
// { getName: () => string; getAge: () => number }

Branded Types: Nominal Typing

TypeScript uses structural typing — two types with the same shape are interchangeable. Branded types achieve nominal typing:

// Without branded types — easy to mix up IDs
type UserId    = string
type ProductId = string

function getUser(id: UserId): Promise<User> { ... }
function getProduct(id: ProductId): Promise<Product> { ... }

// TypeScript allows this — both are just string
const productId: ProductId = 'prod-123'
await getUser(productId)  // No type error, but wrong!

// With branded types
declare const __brand: unique symbol
type Brand<T, B> = T & { [__brand]: B }

type UserId    = Brand<string, 'UserId'>
type ProductId = Brand<string, 'ProductId'>

// Constructor functions enforce the brand
function toUserId(id: string): UserId {
  // Validate format here
  if (!id.startsWith('user-')) throw new Error('Invalid UserId')
  return id as UserId
}

const userId    = toUserId('user-123')
const productId = 'prod-123' as ProductId

await getUser(productId)  // Type error! Cannot assign ProductId to UserId
await getUser(userId)     // OK

Branded types are used extensively in financial systems where mixing currency amounts is catastrophic:

type USD = Brand<number, 'USD'>
type EUR = Brand<number, 'EUR'>

function addUSD(a: USD, b: USD): USD {
  return (a + b) as USD
}

const price: USD = 100 as USD
const euros: EUR = 85 as EUR
addUSD(price, euros)  // Type error — cannot add EUR to USD

Mapped Types

Mapped types transform existing types:

// Built-in utilities implemented with mapped types
type Readonly<T> = { readonly [K in keyof T]: T[K] }
type Partial<T>  = { [K in keyof T]?: T[K] }
type Required<T> = { [K in keyof T]-?: T[K] }

// Make specific keys optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

type User = { id: string; name: string; email: string; bio: string }
type UserUpdate = PartialBy<User, 'bio' | 'email'>
// { id: string; name: string; bio?: string; email?: string }

// Deep readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}

// Filter keys by value type
type KeepStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
}

type UserStrings = KeepStrings<{ id: string; age: number; name: string }>
// { id: string; name: string }

Type Guards

Type guards narrow types at runtime with full type safety:

// User-defined type guard
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'email' in value &&
    typeof (value as any).id === 'string' &&
    typeof (value as any).email === 'string'
  )
}

// Discriminated union type guard
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'rectangle'; width: number; height: number }

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':    return Math.PI * shape.radius ** 2
    case 'square':    return shape.side ** 2
    case 'rectangle': return shape.width * shape.height
    // TypeScript ensures exhaustiveness — adding a new Shape variant
    // without handling it here becomes a compile error
  }
}

// Assertion function
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new TypeError(`Expected string, got ${typeof value}`)
  }
}

function processInput(value: unknown) {
  assertIsString(value)
  // value is narrowed to string here
  return value.toUpperCase()
}

Putting It Together: A Type-Safe API Client

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'

type ApiEndpoints = {
  'GET /users':          { response: User[] }
  'GET /users/:id':      { params: { id: UserId }; response: User }
  'POST /users':         { body: CreateUserDTO; response: User }
  'PATCH /users/:id':    { params: { id: UserId }; body: UpdateUserDTO; response: User }
  'DELETE /users/:id':   { params: { id: UserId }; response: void }
}

type EndpointKey = keyof ApiEndpoints

// Extract parts of the key
type ExtractMethod<K extends EndpointKey> = K extends `${infer M} ${string}` ? M : never
type ExtractPath<K extends EndpointKey>   = K extends `${string} ${infer P}` ? P : never

// Type-safe fetch function
async function apiFetch<K extends EndpointKey>(
  endpoint: K,
  options: Omit<ApiEndpoints[K], 'response'>
): Promise<ApiEndpoints[K]['response']> {
  // Implementation
  const [method, path] = (endpoint as string).split(' ')
  const response = await fetch(buildUrl(path, (options as any).params), {
    method,
    body: (options as any).body ? JSON.stringify((options as any).body) : undefined,
  })
  return response.json()
}

// Usage: fully type-safe
const users = await apiFetch('GET /users', {})
// users is User[]

const user = await apiFetch('GET /users/:id', { params: { id: userId } })
// user is User — and id must be UserId branded type

Advanced TypeScript is a force multiplier. Each hour spent writing precise types saves hours of debugging runtime errors. Start with branded types for your domain identifiers and conditional types for your utility functions — you will wonder how you shipped without them.