我们在定义类型时,除了要给类型具体的规范和约束外,另一个重要考量是能否方便的复用。
为什么要有泛型
假如我们有一个foo
的方法,它的参数是多个字符串,用于将所有字符串参数拼起来:
function foo(...list: string[]): string {
return list.join(",");
}
foo("str1", "str2", "str3", "str4");
// result: str1,str2,str3,str4
现在我们有一个新的需求,传入的参数改为多个数值, 需要把所有的数值拼起来,那我们又得定义一个bar
方法:
function bar(...list: number[]): string {
return list.join(",");
}
bar(1, 2, 3, 4)
// result: 1,2,3,4
可以看出这两个方法实现代码其实是一模一样的,唯一的区别是参数和返回值的类型不同。当然,为了消除这种重复,我们可以把参数和返回值类型改为any
,但这样会失去类型保护;或者我们也可以用联合类型,结合函数重载实现,但也比较繁琐,尤其是可选类型数量很多后。
而通过使用泛型就能很好的解决这个问题,达到类型保护和重用的目的:
function foo<T>(...list: T[]): string {
return list.join(",");
}
// 只能输入string类型变量
foo<string>("str1", "str2", "str3", "str4");
// 只能输入number类型变量
foo<number>(1, 2, 3, 4)
我们使用T
代表类型(泛型名字可以是其他任意字符串),在实际调用函数时再传入参数的类型作为约束。泛型可以应用于函数,接口,类型别名,类等常见类型。
就算在调用foo
方法时,不显式指定类型,ts也能根据实参推导出T
的具体类型:

多个参数
泛型的参数可以是多个,各个参数使用,
隔开:
function merge<T, U>(a: T, b: U): T & U {
return Object.assign(a, b);
}
const result = merge({ name: "xxx" }, { age: 12 });
// result = { name: 'xxx', age: 12 }
指定泛型类型时,要么全部指定,要么都不指定(依赖于ts自动推断)。不能只指定一部分(除非剩下的部分都有默认值):
function fn<T, U>(a: T, b: U) {}
// 合法
fn<string,number>("1", 1);
fn("1", 1);
// 不合法
fn<string>("1", 1);
泛型约束
在函数内部使用泛型变量时,由于不能事先确定它是哪种类型,所以无法随意调用属于它的属性和方法:
function countLength<T>(a: T) {
// 无法获取a.length:Property 'length' does not exist on type 'T'.
console.log("object's length is" + a.length);
}
这时我们可以对T
进行约束,T
必须是一个有length
属性的对象:
interface Lengthy {
length: number;
}
function countLength<T extends Lengthy>(a: T) {
console.log("object's length is" + a.length);
}
我们首先定义了一个Lengthy
的接口,它具有length
属性。并且让T
继承Lengthy
,保证了T
是一个具有length
属性的对象,实际使用时:
// 合法
countLength({ length: 10, name: "baba" });
// 不合法
// Property 'length' is missing in type '{}' but required in type 'Lengthy'
countLength({});
我们重构一下merge
的方法。在merge
方法中,我们虽然期望传入的是两个对象,但实际并没有在约束,所以实际上任何类型都是合法的。虽然Object.assign
对不同的类型的变量做了一定的额外处理保证不报错,但是实际结果可能会和预想结果不同。我们为泛型加上约束,保证传入的参数必须是两个对象:
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return Object.assign(a, b);
}
多个类型之间也可以相互约束:
function copyProperties<T extends U, U extends object>(target: T, source: U) {
for (const key in source) {
(target as U)[key] = source[key];
}
return target;
}
// 合法
copyProperties({ name: "xxx", age: 20 }, { age: 30 });
// 不合法,因为target不兼容source
copyProperties({ name: "xxx" }, { age: 30 });
T
继承U
,保证了T
包含U
上的所有属性。
举个更复杂的例子。我们有一个方法getProperty
,它有两个参数,obj
和key
。它的作用是获取obj[key]
的值:
function getProperty<T extends object>(obj: T, key: string) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // 1
getProperty(x, "m"); // undefined
实际上我们并没有对key
进行约束,我们期望的key
只能是obj
中的属性。此时可以使用keyof
关键字:
interface Obj {
name: string;
age: number;
}
type Key = keyof Obj; // Key = "name" | "age"
经过约束后的方法为:
function getProperty<T extends object, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // 1
getProperty(x, "m"); // Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'
默认值
我们也可以为泛型添加默认类型,这个默认类型会在没有指定类型参数,或者是ts无法从实际参数中推断出类型时生效。
function createArray<T = string>(length: number, value: T): T[] {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
我们使用T = string
给了T
一个默认的类型。注意:
- 如果一个泛型有默认值,那么该泛型就是可选泛型
- 可选泛型声明必须在没有默认值的泛型之后
- 如果泛型有约束,那么默认值必须兼容这个约束
// 可选泛型排在后面
function fn<T, U = string>(a: T, b: U) {}
interface Lengthy {
length: number;
}
// 默认值要兼容Lengthy接口
function countLength<T extends Lengthy = string[]>(a: T) {
console.log("object's length is" + a.length);
}
泛型接口
泛型应用于接口:
interface Model<T> {
data: T;
name: string;
}
const model: Model<number> = {
name: "model",
data: 1,
};
接口也可以表示泛型函数类型:
interface LogArrayFn<T> {
(array: T[]): void;
}
// 实际声明函数时,需要指定泛型
const fn: LogArrayFn<string> = (array) => {
console.log(array);
};
fn(["12"]);
// 另一个形式
interface LogArrayFn {
<T>(array: T[]): void;
}
const fn: LogArrayFn = (array) => {
console.log(array);
};
// 在调用函数时指定泛型,或者依赖ts的自动推断
fn<string>(["12"]);
泛型别名
类型别名也可以使用泛型,其使用方法和接口十分类似:
type AliasModel<T> = {
data: T;
name: string;
};
const t: AliasModel<number> = {
name: "xxx",
data: 1,
};
使用别名声明泛型函数:
// 声明以及使用和接口类型
type AliasLogArrayFn<T> = {
(array: T[]): void;
};
// 或者
type AliasLogArrayFn = {
<T>(array: T[]): void;
};
// 简化版
type AliasLogArrayFn<T> = (array: T[]) => void;
type AliasLogArrayFn = <T>(array: T[]) => void;
泛型类
与泛型接口类似,泛型也可以用于类的类型定义中:
class GenericData<T> {
data: T;
constructor(data: T) {
this.data = data;
}
logData() {
console.log(this.data);
}
}
// 推断出T 为 { name: string }
const data = new GenericData({ name: "xxx" });
如果类只包含一个或多个泛型函数,也可以是:
class GenericData {
logData<T>(data: T) {
console.log(data);
}
}
泛型绑定具体类型时机
对于不同的类型,泛型绑定具体类型的时机(或者是根据参数推断具体类型的时机)是不同的:
- 对于泛型函数:当调用函数时绑定
- 对于泛型类:当实例化时绑定
- 对于泛型接口或泛型类型别名:当使用它们时绑定
这可以解释以下现象:
// 第一种
type AliasLogArrayFn<T> = (array: T[]) => void;
const fn: AliasLogArrayFn<number> = (array) => console.log(array);
fn([1,2])
// 第二种
type AliasLogArrayFn = <T>(array: T[]) => void;
const fn: AliasLogArrayFn = (array) => console.log(array);
fn<number>([1, 2, 3]);
第一种声明方式得到的是一个泛型别名,在使用泛型别名时就需要绑定类型,所以声明fn
时需要显式指定类型,相当于声明了具体类型的方法。
第二种得到的是代表泛型函数的别名(本身不是泛型别名),所以声明fn
时相当于在声明一个泛型方法,不需要绑定类型,而在调用时需要绑定类型。