TypeScript 入门教程:04 — 泛型 (Generics)
2026/7/13 12:55:52 网站建设 项目流程

1. 你以前怎么惨

你写了一个函数,接收一个值,把它包进数组里返回。

functionwrapInArray(value:string):string[]{return[value];}

隔壁同事也要用。但他传进来的是number。你复制了一份:

functionwrapNumberInArray(value:number):number[]{return[value];}

老板又要一个boolean版本。你开始复制第三份。HR 找你谈话,说代码审查没过。

问题不是同事传错了类型。问题是你的函数写死了类型。类型不安全的时候,你只能复制粘贴。类型安全了,你又陷入了复制粘贴的另一种形式——只不过这次编译器逼你复制。

泛型解决这个问题:写一个函数,让它自动适应传给它的类型

2. 先看一眼烂代码

// 三个函数,逻辑一模一样,只是类型标注不同functionwrapString(value:string):string[]{return[value];}functionwrapNumber(value:number):number[]{return[value];}functionwrapBoolean(value:boolean):boolean[]{return[value];}// 更糟糕的:用 any 逃避functionwrapAny(value:any):any[]{return[value];}// any 让你输掉所有类型信息:constresult=wrapAny("hello");// result 的类型是 any[]。TS 不知道里面是 string。// result[0].toUpperCase() —— 编译通过,运行也通过(因为是 string)// result[0].toFixed(2) —— 编译也通过!因为 any 跳过了所有检查// 运行时崩溃:toFixed is not a function

any就像在超市里把所有人的购物车混在一起——你不知道谁的购物车里有鸡蛋,谁的有砖头。出问题了才发现。

3. 用新语法修好它

3.1 泛型函数:<T>

泛型类比自动售货机。投可乐进去,出来可乐。投薯片进去,出来薯片。售货机不关心你投什么,但它保证出来的东西和你投的一样。

// <T> 声明一个类型变量 T。调用时,TS 根据你传入的值推导出 T 是什么。functionwrapInArray<T>(value:T):T[]{return[value];}conststrings=wrapInArray("hello");// strings: string[]constnumbers=wrapInArray(42);// numbers: number[]constbooleans=wrapInArray(true);// booleans: boolean[]// TS 记住了入参类型和返回类型的对应关系。// strings[0].toUpperCase() ✅// strings[0].toFixed(2) ❌ 编译报错

T只是一个名字。叫TValue、叫Item、随便你。当函数只有一个泛型参数时,惯例用T

你也能显式指定类型参数:

constexplicit=wrapInArray<string>("hello");

但绝大多数时候没必要——TS 能自己推导出来。

3.2 泛型约束:<T extends ...>

有时你不接受任意类型。你只接受"有某个属性"的类型。

// 需求:接收一个对象,返回它的某个属性值// 不约束的话,TS 不知道 T 有没有 length 属性functiongetLength<T>(obj:T):number{returnobj.length;// ❌ Property 'length' does not exist on type 'T'}

extends约束 T:

// T extends { length: number } —— T 必须是"有 length 属性的类型"functiongetLength<Textends{length:number}>(obj:T):number{returnobj.length;// ✅ TS 知道 obj 一定有 length}getLength("hello");// ✅ string 有 lengthgetLength([1,2,3]);// ✅ 数组有 lengthgetLength({length:10});// ✅// getLength(42); // ❌ number 没有 length

泛型约束 = “这台售货机只接受饮料,不接受石头”。

一个更实用的例子:

// 确保传入的对象至少有 name 字段functiongreet<Textends{name:string}>(obj:T):string{return`Hello,${obj.name}!`;}greet({name:"Alice",age:30});// ✅ 多出来的属性没关系// greet({ age: 30 }); // ❌ 没有 name

3.3 泛型接口

接口也可以用泛型。标准库的Array<T>Map<K, V>就是泛型接口。

// 一个简单的"键值对"容器interfaceBox<T>{value:T;}conststringBox:Box<string>={value:"hello"};constnumberBox:Box<number>={value:42};// 更实用的例子:API 响应包装interfaceApiResponse<T>{data:T;status:number;message:string;}typeUserResponse=ApiResponse<{id:number;name:string}>;typePostResponse=ApiResponse<{title:string;body:string}>;// 标准库示例constnames:Array<string>=["Alice","Bob"];constscores:Map<string,number>=newMap();scores.set("Alice",95);

3.4 默认泛型参数

有些泛型参数 90% 的情况都是同一个类型。给个默认值。

// 大多数时候,事件数据就是一个简单的字符串interfaceEvent<T=string>{type:string;data:T;}constsimple:Event={type:"click",data:"clicked"};// data: stringconstcomplex:Event<{x:number}>={type:"move",data:{x:100}};// data: { x: number }

默认参数让你不写<string>也能得到默认行为,但需要时还能覆盖。

4. 什么时候不该用

  1. 函数只接受一个具体类型。别写成function add<T>(a: T, b: T)再在内部硬转成 number。直接写function add(a: number, b: number)

  2. 泛型参数没有出现在参数列表中。function create<T>(): T——你能凭空造出一个 T 吗?不能。那就在骗 TS。这种几乎总是反模式。

  3. 用泛型替换简单的联合类型。如果你知道参数只可能是string | number,写联合类型比泛型更清晰。

  4. 为了"酷"而泛型。每个类型参数都增加认知负担。如果一个问题没有重复的类型逻辑,不需要泛型。

5. 小结

语法一句话示例
<T>声明类型变量,输入和输出类型联动function id<T>(x: T): T { return x; }
<T extends U>约束 T 必须是 U 的子类型function len<T extends { length: number }>(x: T): number
Interface<T>接口接受类型参数interface Box<T> { value: T }
<T = U>泛型默认值interface Event<T = string> { data: T }
Array<T>标准库的泛型数组const a: Array<number> = [1, 2]
Map<K, V>标准库的泛型 Mapconst m: Map<string, number> = new Map()

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询