---
title: "TS泛型和extends"
date: "2026-02"
category: "TS"
tags: ["TS"]
description: "\\<T>泛型对象应该是静态语言中比较难以理解的一个概念。但就其本质而言，它就像是一个占位符：通过告诉编译器这里未来会有某个类型——它既可以是已有的类型，也可以是自定义的类型..."
source: "https://enriquemark.com/zh-hans/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
```
