---
title: "TS泛型和extends"
date: "2026-02"
category: "TS"
tags: ["TS"]
description: "\\<T>泛型物件應該是靜態語言中比較難以理解的一個概念。但就其本質而言，它就像是一個佔位符：通過告訴編譯器這裡未來會有某個型別——它既可以是已有的型別，也可以是自定義的型別..."
source: "https://enriquemark.com/zh-hant/posts/TS%E6%B3%9B%E5%9E%8B%E5%92%8Cextends"
---

\<T>泛型物件應該是靜態語言中比較難以理解的一個概念。但就其本質而言，它就像是一個佔位符：通過告訴編譯器這裡未來會有某個型別——它既可以是已有的型別，也可以是自定義的型別。
總之這裡需要有一個型別，該型別會按照我後續的形式進行操作和定義。說白了，就是將這裡的宣告指代過去，或者用一個保留字來指代。這裡的 T 不一定非得寫成 T，寫成什麼都行；但本質上，它就是一個在這裡預定義的「型別變數」，可以指代任何型別。從而在除錯或編寫程式碼時獲得更好的提示支援。因此，泛型的作用非常大，可以讓程式碼在保持型別安全的前提下，獲得極大的通用性和靈活性。

不過雖然可以隨便寫，但是還是有一個約定俗成的習慣，大概就是下面這些：
- `T` = Type（通用型別）
- `K` / `V` = Key / Value（鍵值對場景）
- `E` = Element（集合元素）
- `R` = Return（返回型別）

除此之外，還有一個「泛型約束」很有意思：它可以指定該泛型必須具備某些屬性，且規定這些屬性的型別。它不強制要求其他內容，但必須存在指定的屬性才能通過校驗。也就是說，只有當該指定屬性存在時，程式才會允許繼續執行。

```ts
// T 可以是任何类型，但前提是它得有个 length 属性，且该属性的类型为 number
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength("hello");     // ✓ string 有 length
getLength([1, 2, 3]);   // ✓ array 有 length
getLength(123);         // ✗ number 没有 length，编译报错
```

A extends B 可以理解為 A 需要滿足 B，或者說 A 包含 B ， A 是 B 的超集，或者說 B 是 A 的子集。

如果我後面指定的是一個集合，且該集合裡面有一個屬性，那麼本質上就是說 A 既然包含了這個集合，因而也必然要有其屬性。而這個地方如果寫的是一個空集，那麼它在JS裡就是一個包含了任何非 null/undefined 值的集合。

比如 `T extends {}`和單獨寫個 `T` ，兩者在除了 null/undefined 外沒有其他區別。既然萬物皆物件，那型別本質上也是一個集合，當該集合具備某個屬性的時候，就和這裡的約束條件 B 對上了，或者說是這個集合裡面的屬性對上了，因此 A 就包含了 B。

這樣來理解的話，其實任何繼承本質上都是在構建一個包含了父類的「超集」。因為繼承的子類，必然是在具備父類所有屬性和方法的前提之下，又擁有了自己的屬性和方法，那麼這其實就是一個超集。雖然「繼承」這個詞乍一聽，好像子類應該是父類的子集，但實際上其實是反過來的，也就是父類是子類的子集。

之所以叫父類和子類，其實是從「例項」或者說是「值」的角度來看待的。

就拿 Animal 和 Dog 來說：
1. 所有的 Dog 必然都是 Animal，但反過來說，Animal 卻不一定是 Dog，因為 Animal 本身是一個更抽象的存在。
2. 從這個角度講，其實 Dog 是 Animal 的子集。

但是從屬性的角度來看，Dog 就擁有比 Animal 更多的屬性和方法，因此它們的關係又是反過來的。
- 如果以「抽象程度」來劃分，那肯定是 Animal 更高層級，更抽象，因此是更「大」的。
- 但越往「具體」的方向走，子類所擁有的具體方法和具體屬性肯定就越多，在這個方面，它更「大」。
這是兩個不同的觀察角度。

extends 的使用場景，一般是我需要在方法裡面呼叫泛型的某個屬性時，才必須要這樣指定，否則會因為調用不了而導致編譯器報錯。但如果我不是為了呼叫某個屬性，而只是單純地使用`...`展開，或者說白了，是對它所有的型別進行一個遍歷，那就不需要特別去指定它。

這種情況就是要麼全有，要麼全無；如果要用到具體的某一個屬性，就必須在裡面宣告。這就跟我呼叫某個變數之前要先宣告一樣，因為泛型本身可以是任何型別，編譯器又不知道它會包含哪些屬性，如果你不對它進行最小屬性的宣告，那後面這裡一定會出錯。

```ts
// 这里就不需要特别地进行 extends 声明，因为里面没有调用到具体的属性
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

// 这里就必须得声明了，因为里面调用了其中一个泛型的属性 id，如果不声明就会报错
function merge<T extends { id: number }, U>(obj1: T, obj2: U): T & U {
  console.log(obj1.id); 
  return { ...obj1, ...obj2 };
}
```

比較有意思的是，這裡的 & 符號從屬性的角度來講，是一個並集，也就是最終的輸出型別，必然擁有這兩種型別的全部屬性和方法；但是如果從值的角度上來講，是一個交集，也就是說，要求最終的輸出集合必須既屬於T型別又屬於U型別。

另外還有一個豎線（|），其實這個跟 if 語句裡面的各種符號都一樣：&（與）、|（或）、非（!）。這些符號的使用邏輯都是一致的。當然，在型別判斷裡面沒有「非」這個邏輯。泛型的話，因為它本身就代表任何型別都可以，沒有非很正常。但在具體型別上，也不存在「非某個型別」。因為這意味著除了該具體型別之外的任何一種型別都可以滿足條件，那該集合內的型別就變成無限種，所以ts並沒有提供這種對型別的宣告方式。

另一方面，<>裡面的型別其實就更像宣告一個容器的內容是啥。它告訴我的是：返回的這個型別物件裡面會是怎樣的型別。

```ts
Array<number>      // 数组，里面装的是 number ，也可以写成 number[]
Promise<Response>  // Promise，最终产出的是 Response
Map<string, User>  // Map，里面装着键-值元素，键是 string，值是 User
```
