跳至主要內容

TypeScript 型別體操入門:從工具型別到條件型別

TypeScript 型別體操入門:從工具型別到條件型別

「型別體操」這個詞聽起來有點嚇人,但它描述的是 TypeScript 型別系統的一種進階用法:用型別層面的運算來描述複雜的型別關係。掌握這些技巧,你就能寫出更精確、更安全的型別定義,讓 TypeScript 真正成為你的開發夥伴,而不只是個麻煩的靜態分析工具。

內建工具型別

TypeScript 提供了一組非常實用的內建工具型別,先把這些用熟:

interface User {
  id: number
  name: string
  email: string
  password: string
  createdAt: Date
  role: 'admin' | 'user' | 'guest'
}

// Partial:所有屬性變為可選
type UserUpdate = Partial<User>
// { id?: number; name?: string; email?: string; ... }

// Required:所有屬性變為必填
type StrictUser = Required<User>

// Pick:選取部分屬性
type UserProfile = Pick<User, 'id' | 'name' | 'email'>

// Omit:排除部分屬性
type SafeUser = Omit<User, 'password'>

// Readonly:所有屬性變為唯讀
type ImmutableUser = Readonly<User>

// Record:建立鍵值對型別
type UserMap = Record<string, User>
type RolePermissions = Record<User['role'], string[]>

// Exclude:從 union 中排除特定型別
type NonAdmin = Exclude<User['role'], 'admin'>  // 'user' | 'guest'

// Extract:從 union 中取出特定型別
type AdminOnly = Extract<User['role'], 'admin'>  // 'admin'

// NonNullable:移除 null 和 undefined
type NonNullString = NonNullable<string | null | undefined>  // string

// ReturnType:取得函式回傳型別
function getUser() { return {} as User }
type GetUserReturn = ReturnType<typeof getUser>  // User

// Parameters:取得函式參數型別
function createUser(name: string, email: string, role: User['role']) {}
type CreateUserParams = Parameters<typeof createUser>  // [string, string, 'admin' | 'user' | 'guest']

// Awaited:取得 Promise 解析後的型別
type AsyncUser = Promise<User>
type ResolvedUser = Awaited<AsyncUser>  // User

條件型別(Conditional Types)

條件型別讓你可以根據型別關係來決定型別,語法類似三元運算子:

// T extends U ? X : Y
type IsString<T> = T extends string ? true : false

type A = IsString<string>   // true
type B = IsString<number>   // false
type C = IsString<'hello'>  // true('hello' extends string)

// 更實用的例子:取得陣列元素型別
type ElementType<T> = T extends Array<infer U> ? U : T

type E1 = ElementType<string[]>   // string
type E2 = ElementType<number[]>   // number
type E3 = ElementType<string>     // string(不是陣列,回傳自身)

infer 關鍵字

infer 讓你在條件型別中「推斷」並取得一個型別:

// 取得函式回傳型別(模擬 ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

// 取得 Promise 解析型別(模擬 Awaited)
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T

// 取得元組第一個元素
type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never
type H = Head<[string, number, boolean]>  // string

// 取得元組最後一個元素
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never
type L = Last<[string, number, boolean]>  // boolean

// 取得函式第一個參數
type FirstParam<T extends (...args: any[]) => any> =
  T extends (first: infer F, ...rest: any[]) => any ? F : never

映射型別(Mapped Types)

映射型別讓你可以基於現有型別的鍵來建立新型別:

// 模擬 Readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}

// 模擬 Partial
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}

// 深度 Partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K]
}

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

// 鍵重映射(as 語法,TypeScript 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

// 用法
type UserGetters = Getters<User>
// { getId: () => number; getName: () => string; getEmail: () => string; ... }

// 過濾特定型別的屬性
type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
}

type StringFields = FilterByType<User, string>
// { name: string; email: string; password: string }

模板字面量型別

type EventName = 'click' | 'focus' | 'blur'

// 生成事件處理器型別名稱
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'

// 組合多個 union
type Axis = 'x' | 'y'
type Size = 'small' | 'medium' | 'large'
type ButtonVariant = `${Size}-${Axis}`
// 'small-x' | 'small-y' | 'medium-x' | 'medium-y' | 'large-x' | 'large-y'

// 解析路由參數
type ParseParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ParseParams<`/${Rest}`>
    : T extends `${string}:${infer Param}`
      ? Param
      : never

type Params = ParseParams<'/users/:userId/posts/:postId'>
// 'userId' | 'postId'

實際應用範例

型別安全的事件系統

type EventMap = {
  'user:login': { userId: string; timestamp: Date }
  'user:logout': { userId: string }
  'post:created': { postId: string; title: string; authorId: string }
  'post:deleted': { postId: string }
}

type EventEmitter = {
  on<K extends keyof EventMap>(
    event: K,
    listener: (data: EventMap[K]) => void
  ): void

  emit<K extends keyof EventMap>(
    event: K,
    data: EventMap[K]
  ): void
}

declare const emitter: EventEmitter

// 完全型別安全
emitter.on('user:login', (data) => {
  console.log(data.userId)      // string
  console.log(data.timestamp)   // Date
})

emitter.emit('post:created', {
  postId: '123',
  title: '新文章',
  authorId: 'user-456'
})
// 如果缺少任何欄位,TypeScript 會報錯

型別安全的 API 客戶端

type ApiEndpoints = {
  'GET /users': { response: User[] }
  'GET /users/:id': { params: { id: string }; response: User }
  'POST /users': { body: Omit<User, 'id' | 'createdAt'>; response: User }
  'DELETE /users/:id': { params: { id: string }; response: void }
}

type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
type EndpointKey = keyof ApiEndpoints

type RequestConfig<K extends EndpointKey> = ApiEndpoints[K]

async function apiCall<K extends EndpointKey>(
  endpoint: K,
  config: Omit<RequestConfig<K>, 'response'>
): Promise<RequestConfig<K> extends { response: infer R } ? R : never> {
  // 實作細節...
  return null as any
}

// 使用時完全型別安全
const users = await apiCall('GET /users', {})
// users 的型別是 User[]

const user = await apiCall('GET /users/:id', { params: { id: '123' } })
// user 的型別是 User

總結

TypeScript 的型別系統是圖靈完備的,理論上可以在型別層面做任何計算。但型別體操的目標不是炫技,而是讓你的程式碼更安全、自動補全更精準、錯誤更早被發現。從內建工具型別開始,逐步理解條件型別和映射型別,你就能應對絕大多數的實際需求。

分享這篇文章