阮一峰《TypeScript 教程》学习笔记——泛型
1. 一段话总结
泛型是 TypeScript 中解决“输入与输出类型关联”的核心机制,通过类型参数(如) 建立类型逻辑,使函数、接口、类、类型别名能适配多种类型且保持类型一致性;其主要应用于函数、接口、类、类型别名四种场合,支持为类型参数设置默认值(如<T = string>) 和约束条件(如<T extends { length: number }>,限制类型参数需满足特定结构);数组的泛型表示为Array<T>(T[]是简写),TypeScript 原生数据结构(如Map、Set、Promise)也基于泛型实现;使用时需注意尽量少用、类型参数越少越好、类型参数需出现两次及以上、支持泛型嵌套,避免过度复杂。
2. 思维导图

3. 详细总结
一、泛型简介
-
核心问题
当函数、接口等需适配多种类型,且输入与输出类型存在关联时(如函数返回值类型与参数类型一致),直接用any会丢失类型检查,泛型通过类型参数解决此问题。
示例:// 无泛型:无法关联参数与返回值类型 function getFirst(arr:any[]):any { return arr[0]; } // 有泛型:明确参数(T[])与返回值(T)类型关联 function getFirst<T>(arr:T[]):T { return arr[0]; } -
类型参数
- 语法:用尖括号包裹(如
<T>),名称自定义(习惯用T、U、V等大写字母); - 调用方式:显式传入(如
getFirst<number>([1,2,3]))或自动推断(如getFirst([1,2,3]),TS推断T=number); - 多类型参数:用逗号分隔(如
<T,U>,适用于多输入类型关联,如map<T,U>(arr:T[], f:(arg:T)=>U):U[])。
- 语法:用尖括号包裹(如
二、泛型的四种写法
| 应用场合 | 基础语法 | 关键特性 | 示例 |
|---|---|---|---|
| 函数泛型 | 函数名后加类型参数 | 支持显式/自动推断类型参数;变量形式有两种写法 | function id<T>(arg:T):T { return arg; } 变量写法: let myId:<T>(arg:T)=>T = id |
| 接口泛型 | 接口名后加类型参数(全局)/方法内加类型参数(局部) | 全局类型参数:接口内所有成员可用; 局部类型参数:仅对应方法可用 | 全局:interface Box<Type> { contents:Type } 局部: interface Fn { <Type>(arg:Type):Type } |
| 类泛型 | 类名后加类型参数 | 类型参数仅用于实例成员(静态成员不可引用); 继承时需指定父类类型参数 | class Pair<K,V> { key:K; value:V } 继承: class B extends A<any> {} |
| 类型别名泛型 | 类型别名后加类型参数 | 支持递归定义(如树形结构); 可组合其他泛型 | `type Nullable = T |
三、类型参数的特性
1. 类型参数的默认值
- 语法:
<T = 默认类型>,调用时若未指定类型参数且无法推断,使用默认值; - 规则:
- 默认值可被类型推断覆盖(如
function getFirst<T = string>(arr:T[]):T,调用getFirst([1,2])时T被推断为number,覆盖默认值string); - 可选类型参数(有默认值)需在必选参数后(如
<T, U = boolean>正确,<T = boolean, U>错误);
- 默认值可被类型推断覆盖(如
- 示例:
class Generic<T = string> {list:T[] = [];add(t:T) { this.list.push(t); } } const g1 = new Generic(); // T=string,add('hello')正确,add(4)报错 const g2 = new Generic<number>(); // T=number,add(4)正确,add('hello')报错
2. 类型参数的约束条件
- 语法:
T extends 约束类型,限制类型参数需为“约束类型”的子类型(满足约束类型的结构); - 规则:
- 约束类型可为对象、接口等具体结构(如
<T extends { length: number }>,要求T有length属性); - 多类型参数可互相引用(如
<T, U extends T>,U需是T的子类型); - 不可自引用(如
<T extends T>)或互相约束(如<T extends U, U extends T>); - 默认值需满足约束条件(如
<A extends string, B extends string = 'world'>,B的默认值’world’符合string约束);
- 约束类型可为对象、接口等具体结构(如
- 示例:
// 约束T需有length属性 function comp<T extends { length: number }>(a:T, b:T) {return a.length >= b.length ? a : b; } comp([1,2], [1,2,3]); // 正确(数组有length) comp('ab', 'abc'); // 正确(字符串有length) comp(1, 2); // 报错(数字无length)
四、数组与原生泛型结构
-
数组的泛型表示
- 完整写法:
Array<T>(T为数组成员类型); - 简写形式:
T[](如number[]等同于Array<number>); - TypeScript 内部
Array是泛型接口,定义了length、push()、pop()等成员,确保操作与成员类型一致。
- 完整写法:
-
原生泛型结构
TypeScript 原生数据结构均基于泛型实现,确保类型安全:结构 泛型写法 说明 Map Map<K, V>K为键类型,V为值类型 Set Set<T>T为集合成员类型 Promise Promise<T>T为Promise resolve类型 只读数组 ReadonlyArray<T>不可修改的数组,无push() 示例(只读数组):
function doStuff(values:ReadonlyArray<string>) {values.push('hello'); // 报错(ReadonlyArray无push()) }
五、使用注意点
- 尽量少用泛型:泛型会增加代码复杂性,可不用则不用(如简单类型场景直接用具体类型);
- 类型参数越少越好:多一个类型参数增加一次替换步骤,简化类型逻辑(如可将多余类型参数合并到函数参数类型中);
示例优化:// 优化前(2个类型参数,Fn多余) function filter<T, Fn extends (arg:T)=>boolean>(arr:T[], func:Fn):T[] { return arr.filter(func); } // 优化后(1个类型参数,直接写函数类型) function filter<T>(arr:T[], func:(arg:T)=>boolean):T[] { return arr.filter(func); } - 类型参数需出现两次及以上:若类型参数仅出现一次,可简化为具体类型(如
<Str extends string>(s:Str)可简化为s:string); - 支持泛型嵌套:类型参数可引用其他泛型,实现复杂类型组合(如
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>)。
4. 关键问题
问题1:泛型的核心作用是什么?它相比直接使用any有什么优势?
答案:
泛型的核心作用是建立输入与输出的类型关联,使函数、接口、类等能适配多种类型且不丢失类型检查(如函数参数为T[]时,返回值必为T,确保类型一致性)。
相比any,其优势在于:
- 保留类型检查:
any会关闭类型检查,导致错误在运行时暴露(如function getFirst(arr:any[]):any,返回值可被误当作任意类型使用);而泛型通过类型参数锁定类型关系(如function getFirst<T>(arr:T[]):T,返回值类型与参数类型强关联,误用会编译报错); - 增强代码可读性:泛型明确了“类型适配逻辑”,读者可通过类型参数(如)快速理解输入输出的类型关系,而
any无法体现类型约束。
问题2:类型参数的“约束条件(extends)”和“默认值”可以同时使用吗?有哪些关键规则需要遵守?
答案:
可以同时使用,但需满足“默认值必须符合约束条件”,关键规则如下:
- 默认值需符合约束:若类型参数同时有约束和默认值,默认值必须是约束类型的子类型(如
<A extends string, B extends string = 'world'>,默认值’world’是string的子类型,符合约束); - 可选参数顺序:有默认值的类型参数(可选)需在必选类型参数之后(如
<T, U = boolean>正确,<T = boolean, U>错误); - 约束不可自引用/互相约束:约束条件不能是类型参数自身(如
<T extends T>),也不能多参数互相约束(如<T extends U, U extends T>),否则会导致约束无效;
示例(合法使用):
type Fn<A extends string, B extends string = 'world'> = [A, B];
type Result1 = Fn<'hello'>; // ["hello", "world"](使用默认值,符合约束)
type Result2 = Fn<'hi', 'ts'>; // ["hi", "ts"](自定义值,符合约束)
问题3:泛型在类中的使用有什么特殊限制?为什么静态成员不能引用类型参数?
答案:
泛型在类中的特殊限制是静态成员(静态属性、静态方法)不能引用类型参数,原因如下:
- 类型参数的作用域:类的类型参数(如
class C<T>)是“实例级”的,仅用于描述实例的属性和方法(每个实例可指定不同的T,如new C<number>()和new C<string>()是不同实例类型); - 静态成员的作用域:静态成员是“类级”的,属于类本身而非实例,在类定义时就已确定,无法关联到实例级的类型参数(若静态成员引用T,类的所有实例会共享该静态成员,导致T的类型混乱);
示例(错误与正确对比):
class C<T> {static data: T; // 报错(静态成员引用实例级类型参数T)instanceData: T; // 正确(实例属性引用T)constructor(data:T) { this.instanceData = data; }
}
