泛型
泛型(Generics)讓你寫一份函式或型別定義,套用到多種不同型別上,同時保留型別檢查——不用犧牲安全性換取重用性,也不用退回 any。
為什麼需要泛型
先看沒有泛型會遇到的問題:
function firstAny(arr: any[]): any {
return arr[0]
}
const num = firstAny([1, 2, 3]) // 型別是 any,失去型別檢查
num.toUpperCase() // 編譯不會報錯,但執行期會噴錯
用 any 雖然能處理任何陣列,但回傳值也變成 any,等於放棄了型別檢查。泛型可以保留「輸入是什麼型別,輸出就是什麼型別」的關聯:
function first<T>(arr: T[]): T {
return arr[0]
}
const num = first([1, 2, 3]) // T 被推論為 number,num 是 number
const str = first(["a", "b"]) // T 被推論為 string,str 是 string
// num.toUpperCase() // 編譯錯誤:number 沒有 toUpperCase
<T> 是型別參數,呼叫時 TypeScript 會依傳入的引數自動推論出 T 是什麼型別,也可以手動指定:first<number>([1, 2, 3])。
泛型 interface 與 type
interface ApiResponse<T> {
data: T
status: number
}
const userResponse: ApiResponse<{ id: number; name: string }> = {
data: { id: 1, name: "Alice" },
status: 200,
}
API 回應的外層結構(data、status)都一樣,只有 data 裡面的實際資料型別不同——這正是泛型最常見的使用情境:一份 ApiResponse 定義,套用到所有 API。
泛型限制(Constraints)
interface HasLength {
length: number
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length)
return item
}
logLength("hello") // OK,字串有 length
logLength([1, 2, 3]) // OK,陣列有 length
// logLength(123) // 編譯錯誤:number 沒有 length
extends 在這裡是限制 T 必須符合某個形狀,不是類別繼承。沒有限制時 T 可以是任何型別,導致你在函式裡幾乎不能對 T 做任何操作;加上 extends HasLength,TypeScript 才知道 item 一定有 .length 可以用。
相關影音
Net Ninja 這支專講泛型,從「為什麼需要泛型」講到泛型函式與泛型 interface,對應本頁全部內容,已嵌在 進階型別總覽。