Skip to content
泛型基本用法:函数与接口
概述
泛型函数允许在函数签名中引入类型参数,从而在调用时确定具体类型。声明方式主要有三种,其类型参数放置位置略有差异,但语义等价。泛型接口则将类型参数提升到接口层级,使多个成员共享同一类型约束。
基本概念
- 类型参数:泛型声明中的占位类型名,通常写作
T、U、K、V,在调用时由具体类型替换。 - 类型推断:TypeScript 根据实际传入值自动确定类型参数,无需显式标注。
- 泛型调用签名:类型参数属于方法签名本身,而非整个接口的泛型形式。
基本用法
泛型函数的声明
命名函数
类型参数写在函数名之后、参数列表之前:
typescript
function identity<T>(value: T): T {
return value;
}<T> 紧跟在函数名之后,参数 value 的类型与返回类型均为 T,编译器据此建立起“输入与输出类型一致”的约束。
函数表达式
将匿名函数赋值给变量时,类型参数写在等号右侧的函数定义上:
typescript
const identity = function <T>(value: T): T {
return value;
};此处的 <T> 与命名函数声明中的语义一致。变量 identity 的类型会被自动推断为一个泛型调用签名。
箭头函数
类型参数放在参数列表的圆括号前面:
typescript
const identity = <T>(value: T): T => value;泛型箭头函数必须保留参数外面的圆括号。如果省略括号写成 <T>value => value,<T> 在函数上下文之外会被解析为 JSX 标签或比较运算符。保留圆括号能够消除这一歧义。
在 .tsx 文件中,箭头函数的 <T> 还会被优先解析为 JSX 元素标签。一种解决方式是在类型参数后面追加一个逗号:
typescript
const identity = <T,>(value: T): T => value;也可以用函数表达式替代箭头函数来规避解析冲突。
类型参数的传递方式
泛型函数有两种传入类型参数的方式。
显式传参
调用时在函数名后面直接写出具体类型:
typescript
const result = identity<string>("hello");
// result 类型为 string<string> 明确告知编译器 T 应替换为 string。当类型推断信息不足或需要强调调用意图时,显式传参可以让类型关系更清晰。
自动推断
省略类型参数,让编译器根据实参推导:
typescript
const result = identity("hello");
// result 类型仍为 string编译器从参数 "hello" 推导出 T = string,进而确定返回类型为 string。多数场景下自动推断足以给出正确结果,同时减少冗余的类型书写。
两种调用方式在编译后的行为没有差异——泛型信息在运行时会被擦除,只保留 JavaScript 代码。
多类型参数
一个函数可以声明多个类型参数,并在它们之间建立映射关系:
typescript
function mapArray<T, U>(arr: T[], fn: (item: T) => U): U[] {
const result: U[] = [];
for (const item of arr) {
result.push(fn(item));
}
return result;
}
const lengths = mapArray(["hello", "world"], s => s.length);
// lengths 推断为 number[],T 推断为 string,U 推断为 numberT 表示原始数组的元素类型,U 表示转换后的类型。显式传参时同样可以约束调用的类型:
typescript
const parsed = mapArray<string, number>(["1", "2"], s => parseInt(s, 10));多类型参数的核心作用是表达不同类型之间的映射或组合关系,而不仅仅是单类型占位。
泛型接口
接口声明可以携带类型参数:
typescript
interface Container<T> {
value: T;
getValue(): T;
}Container<T> 本身还不是具体类型,需要通过类型参数实例化后才能用于声明变量:
typescript
const stringContainer: Container<string> = {
value: "text",
getValue() {
return this.value;
}
};Container<string> 是实例化后的具体类型,所有 T 均被替换为 string。不同实例化之间(如 Container<string> 与 Container<number>)没有子类型关系,除非借助泛型约束或联合类型,否则不能相互赋值。
泛型调用签名
类型参数可以出现在接口的方法签名级别,而非整个接口级别:
typescript
interface GenericFn {
<T>(value: T): T;
}<T> 属于调用签名本身,接口 GenericFn 本身不是泛型的——声明 const fn: GenericFn 时不需要提供类型参数。类型参数在每次调用时确定:
typescript
const identity: GenericFn = <T>(value: T) => value;
const a = identity("hello"); // a 类型为 string
const b = identity(42); // b 类型为 number这种结构适用于“可用于任意类型的单一函数”。
作为对比,如果改为 interface GenericFn<T> { (value: T): T },则 T 在赋值 const fn: GenericFn<string> 时就被固定,后续调用无法再改变。方法级泛型适合多态函数,接口级泛型适合在多个成员之间共享同一个具体类型的结构。
函数类型中的泛型
类型别名也可以定义泛型函数类型:
typescript
type MapFn<T, U> = (arr: T[], fn: (item: T) => U) => U[];
const stringMap: MapFn<string, string> = (arr, fn) => arr.map(fn);使用接口表达相同的形式:
typescript
interface MapFn<T, U> {
(arr: T[], fn: (item: T) => U): U[];
}两者在类型检查中行为一致。类型别名更简洁,适合单行调用签名;接口则适合需要后续扩展(例如混入更多属性)的场景。
注意点
.tsx文件中的箭头函数泛型:除追加逗号<T,>外,也可以用函数表达式替代。该问题仅出现在扩展名为.tsx且开启 JSX 支持的项目中。- 类型参数命名:单个类型参数通常命名为
T,多个时可用T、U、V,或者更具描述性的名称如TElement、TKey。避免在通用工具函数中混入业务含义过强的名字,这会降低复用性。 - 类型推断的局限:当类型参数只出现在返回值而未出现在参数上时,编译器无法推断,必须显式传入。例如
function get<T>(): T无法通过get()推断T。 - 泛型接口的实例化:如果同一文件中频繁使用
Container<string>和Container<number>,TypeScript 会在编译期分别为这两种具体类型执行成员检查。类型系统不会在运行时“创建”多个类型,但在类型运算层面它们仍是两个独立的结构。 - 运行时擦除:泛型信息只存在于编译阶段,运行时无法通过
instanceof检查泛型类型,也无法动态获取泛型参数的具体类型。这与 Java 的类型擦除有相似的约束。
