TypeScript 泛型實戰指南:打造可複用的型別安全程式碼
11 分鐘閱讀 1,200 字
TypeScript 泛型實戰指南:打造可複用的型別安全程式碼
泛型(Generics)是 TypeScript 中最強大也最常被誤解的特性之一。許多開發者遇到複雜的泛型語法就跳過,但其實只要掌握核心概念,泛型能讓你寫出既靈活又安全的程式碼。
為什麼需要泛型
考慮一個簡單的函式,取得陣列的第一個元素:
// 沒有泛型:失去型別資訊
function first(arr: any[]): any {
return arr[0]
}
const num = first([1, 2, 3]) // 型別:any,IDE 無法自動補全
// 使用泛型:保留型別資訊
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
const num = first([1, 2, 3]) // 型別:number
const str = first(['a', 'b']) // 型別:string泛型讓函式、類別、介面可以處理多種型別,同時保持完整的型別安全。
泛型約束(Constraints)
使用 extends 限制泛型的範圍:
// 只接受有 length 屬性的型別
function getLength<T extends { length: number }>(value: T): number {
return value.length
}
getLength("hello") // ✅ string 有 length
getLength([1, 2, 3]) // ✅ array 有 length
getLength(123) // ❌ 型別錯誤:number 沒有 length
// 約束為物件的鍵名
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: "Alice", age: 30 }
const name = getProperty(user, "name") // 型別:string
const age = getProperty(user, "age") // 型別:number
getProperty(user, "email") // ❌ 型別錯誤實用工具型別的實作原理
TypeScript 內建的 Partial、Required、Pick、Omit 等工具型別都是泛型實作的:
// Partial<T>:所有屬性變為可選
type MyPartial<T> = {
[K in keyof T]?: T[K]
}
// Required<T>:所有屬性變為必填
type MyRequired<T> = {
[K in keyof T]-?: T[K] // -? 移除可選標記
}
// Pick<T, K>:只保留指定屬性
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
// Omit<T, K>:排除指定屬性
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>
// 使用範例
interface User {
id: number
name: string
email: string
password: string
}
type UserPreview = Pick<User, "id" | "name"> // 只有 id 和 name
type UserUpdate = Partial<Omit<User, "id">> // 排除 id,其他都可選泛型函式與多型別參數
// 合併兩個物件,型別安全地
function merge<T extends object, U extends object>(target: T, source: U): T & U {
return { ...target, ...source }
}
const result = merge(
{ name: "Alice" },
{ age: 30, role: "admin" }
)
// result 的型別:{ name: string } & { age: number; role: string }
console.log(result.name) // ✅
console.log(result.age) // ✅
// 型別安全的事件發射器
type EventMap = {
click: { x: number; y: number }
keydown: { key: string; code: string }
resize: { width: number; height: number }
}
class TypedEventEmitter<Events extends Record<string, any>> {
private listeners: Partial<{
[K in keyof Events]: Array<(data: Events[K]) => void>
}> = {}
on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event]!.push(listener)
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners[event]?.forEach(listener => listener(data))
}
}
const emitter = new TypedEventEmitter<EventMap>()
emitter.on("click", ({ x, y }) => console.log(x, y)) // 型別正確推斷
emitter.emit("click", { x: 10, y: 20 }) // ✅
emitter.emit("click", { key: "a" }) // ❌ 型別錯誤實戰:泛型 API 客戶端
interface ApiResponse<T> {
data: T
status: number
message: string
}
interface PaginatedResponse<T> {
items: T[]
total: number
page: number
pageSize: number
}
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json()
}
async function fetchPaginated<T>(
url: string,
page: number,
pageSize: number
): Promise<PaginatedResponse<T>> {
const params = new URLSearchParams({
page: String(page),
pageSize: String(pageSize)
})
const response = await fetchApi<PaginatedResponse<T>>(`${url}?${params}`)
return response.data
}
// 使用
interface Post {
id: number
title: string
content: string
}
const posts = await fetchPaginated<Post>("/api/posts", 1, 20)
const firstPost = posts.items[0] // 型別:Post,完整型別推斷條件型別入門
// 根據條件選擇不同型別
type IsArray<T> = T extends any[] ? true : false
type A = IsArray<string[]> // true
type B = IsArray<string> // false
// 提取 Promise 的內部型別
type Awaited<T> = T extends Promise<infer U> ? U : T
type C = Awaited<Promise<string>> // string
type D = Awaited<number> // number
// 從函式型別提取返回值
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never總結
泛型是 TypeScript 型別系統的核心,從簡單的型別參數到複雜的條件型別,它讓程式碼在保持靈活性的同時不失型別安全。建議從實際需求出發,先掌握基本泛型和約束,再逐步探索映射型別和條件型別。
分享這篇文章