正在加载,请稍候…

TypeScript 高级模式:条件类型、模板字面量类型与品牌类型

掌握高级 TypeScript 模式,包括条件类型、模板字面量类型、品牌类型、类型守卫、映射类型和 infer 关键字,实现生产级类型安全。

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

超越基础 TypeScript

大多数 TypeScript 指南涵盖接口、泛型和联合类型。本指南涵盖区分 TypeScript 专家与初学者的特性:类型级编程构造,让你在编译时表达复杂的不变量,消除整类运行时错误。

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

条件类型

条件类型让 TypeScript 类型系统做出决策:

// 基本形式:T extends U ? TrueType : FalseType
type IsString<T> = T extends string ? true : false

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

// 提取数组的元素类型
type ElementType<T> = T extends (infer E)[] ? E : never

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

分布式条件类型

当对联合类型应用条件类型时,它会分发到每个成员:

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

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

// 通过包装在元组中阻止分发:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Combined = ToArrayNonDist<string | number>  // (string | number)[]

infer 关键字

infer 在条件类型中捕获一个类型:

// 提取返回类型(TypeScript 内置了 ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

// 提取 Promise 解析的类型
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T

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

// 提取第一个参数
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never
type EmailParam = FirstParam<(email: string, verified: boolean) => void>  // string

// 从嵌套结构中深度提取
type UnpackNested<T> = T extends { data: { items: (infer I)[] } } ? I : never
type Item = UnpackNested<{ data: { items: User[] } }>  // User

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

模板字面量类型

模板字面量类型以编程方式生成字符串类型:

// 基本组合
type Greeting = `Hello, ${string}!`
const g1: Greeting = 'Hello, World!'  // OK
const g2: Greeting = 'Hi, World!'     // Error

// 组合联合——笛卡尔积
type Color  = 'red' | 'green' | 'blue'
type Shade  = 'light' | 'dark'
type ColorVariant = `${Shade}-${Color}`
// "light-red" | "light-green" | ... | "dark-blue"

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

类型安全的事件系统

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' 不在 EventAction 中

字符串操作类型

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

// 从数据对象派生的 getter 类型
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 }

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

品牌类型:名义类型

TypeScript 使用结构类型——两个形状相同的类型可以互换。品牌类型实现了名义类型:

// 没有品牌类型——容易混淆 ID
type UserId    = string
type ProductId = string

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

// TypeScript 允许这样——两者都只是 string
const productId: ProductId = 'prod-123'
await getUser(productId)  // 没有类型错误,但错了!

// 使用品牌类型
declare const __brand: unique symbol
type Brand<T, B> = T & { [__brand]: B }

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

// 构造函数强制品牌
function toUserId(id: string): UserId {
  // 在此处验证格式
  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)  // 类型错误!不能将 ProductId 赋值给 UserId
await getUser(userId)     // OK

品牌类型在金融系统中广泛使用,因为混淆货币金额是灾难性的:

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)  // 类型错误——不能将 EUR 加到 USD

映射类型

映射类型转换现有类型:

// 使用映射类型实现的内置工具类型
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] }

// 使特定键可选
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 }

// 深度只读
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}

// 按值类型过滤键
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 }

类型守卫

类型守卫在运行时缩小类型,同时保持完全类型安全:

// 用户定义的类型守卫
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'
  )
}

// 可辨识联合类型守卫
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 确保穷尽性——添加新的 Shape 变体
    // 而不在此处处理将变成编译错误
  }
}

// 断言函数
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 被缩小为 string
  return value.toUpperCase()
}

综合运用:类型安全的 API 客户端

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

// 提取键的组成部分
type ExtractMethod<K extends EndpointKey> = K extends `${infer M} ${string}` ? M : never
type ExtractPath<K extends EndpointKey>   = K extends `${string} ${infer P}` ? P : never

// 类型安全的 fetch 函数
async function apiFetch<K extends EndpointKey>(
  endpoint: K,
  options: Omit<ApiEndpoints[K], 'response'>
): Promise<ApiEndpoints[K]['response']> {
  // 实现
  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()
}

// 使用:完全类型安全
const users = await apiFetch('GET /users', {})
// users 是 User[]

const user = await apiFetch('GET /users/:id', { params: { id: userId } })
// user 是 User——且 id 必须是 UserId 品牌类型

高级 TypeScript 是一个力量倍增器。花在编写精确类型上的每一小时都能节省数小时的运行时错误调试时间。从领域标识符的品牌类型和工具函数的条件类型开始——你会惊讶于以前没有它们是如何交付的。