泛型
在编写代码的过程中,往往需要考虑代码的通用性。如果一些代码模块不仅可以支持当前已有的数据类型,还支持以后可能定义的数据类型,那么这些代码模块就具备良好的通用性。
使用泛型创建具备通用性的代码模块,这些代码模块可以支持多种类型的数据,编程者可以根据自己的需求,指定具体的数据类型来复用该代码模块。
泛型的基础用法
假设现在需要声明一个通用函数,它支持传入任何类型的参数,然后对这个参数值进行检测,如果检测通过,则返回该参数值。在不使用泛型的情况下,该函数的代码可能如下。
function paramCheck(param: any): any {
if (isValidParam(param))
return param;
return null;
}
然而,将 any
作为参数和返回值会绕过编译检查,这首先会带来出错的风险,其次会使该函数丢失一部分关键信息,即传入参数和返回值的类型应该是相同的。例如,虽然可以传入一个 number
类型的值,但返回的类型在运行代码之前并不确定。
在不使用泛型的情况下,也可以为每种类型的值声明一个处理对应类型的函数。例如,以下代码虽然在编译方面更安全,但该函数完全失去了通用性,且代码会相当冗余。
function paramCheckForNumber(param: number): number {
//...
}
function paramCheckForString(param: string): string {
//...
}
要使函数既保持精简与通用,又不会绕过编译检查,可以使用泛型。例如,以下代码定义了一个泛型函数,在函数后面的尖括号中定义了一个泛型参数 T
,该参数表示一种可传入的类型,然后定义了函数的参数 param
,它是 T
类型,同时该函数的返回值也是 T
类型。
function paramCheck<T>(param: T): T {
if (isValidParam(param))
return param;
return null;
}
在调用泛型函数 paramCheck()
时,需要为泛型参数 T
指定一个具体类型,该函数的参数 param
及返回值均属于该类型。示例代码如下。
let string1 = paramCheck<string>("hello");
let number1 = paramCheck<number>(1);
let bool1 = paramCheck<boolean>(true);
let array1 = paramCheck<number[]>([1, 2, 3]);
let object1 = paramCheck<{ x: string }>({ x: "hello" });
如果为泛型参数指定了具体类型,但传入的函数参数的值不是该类型的,则会引起编译错误,示例代码如下。
//编译错误:类型"number"的参数不能赋给类型"string"的参数。ts(2345)
let x = paramCheck<string>(1);
因为 TypeScript 的编译器能够根据函数实际传入的参数值的类型自动推断出泛型参数的实际类型,所以对于泛型函数来说,不必显式指定泛型参数的具体类型。例如,在以下代码中,虽然没有指明泛型参数 T
的类型,但是编译器能够根据传入参数的具体类型推断出泛型参数 T
的类型。
let string1 = paramCheck("hello");
let number1 = paramCheck(1);
let bool1 = paramCheck(true);
let array1 = paramCheck([1, 2, 3]);
let object1 = paramCheck({ x: "hello" });
在函数中使用泛型
上一节已经讲解了泛型函数的基础用法。要声明泛型函数,需要在函数名称后使用尖括号指定一个到多个泛型参数,具体语法如下。
function 函数名称<泛型参数1,泛型参数2,泛型参数3,...>(参数1:类型1,参数2:类型2,参数3:类型3,...): 返回值类型 {
}
这些泛型参数可以作为函数的各个参数的类型,也可以作为返回值的类型。当然,函数的各个参数和返回值也可以是普通类型。
例如,以下代码声明了一个泛型函数 connect()
,它拥有两个泛型参数,并分别作为函数参数 arg1
和 arg2
的参数类型,然后定义了一个 connectAsBool
参数,用于确定 arg1
和 arg2
两个参数在函数体中是按照布尔类型拼接的还是按照字符串类型拼接的。
function connect<T1, T2>(arg1: T1, arg2: T2, connectAsBool: boolean): boolean |
string {
if (connectAsBool)
return Boolean(arg1) && Boolean(arg2);
else
return String(arg1) + String(arg2);
}
//a的值为true。1和1n会分别转换为布尔类型true,true && true的结果为true
let a = connect<number, bigint>(1, 1n, true);
//b的值为"abcdfalse","abcd"和false会分别转换为字符串类型来进行拼接
let b = connect<string, boolean>("abcd", false;
注意,由于泛型支持任意类型,因此它和 unknown
类型类似,在没有明确类型前无法进行具体的操作。例如,以下代码将引起编译错误。
function addNumber<T>(target: T, num: number, addType: string): T {
if (addType == "add as number") {
//编译错误:运算符"+"不能应用于类型"T"和"number"。ts(2365)
let result = target + num;
return result;
}
else if (addType == "add as array") {
//编译错误:类型"T"上不存在属性"push"。ts(2339)
target.push(num);
return target;
}
return target;
}
就像使用 unknown
类型一样,你必须先通过类型断言或类型防护(详见第 11 章)将泛型类型的变量转换为某种已知的具体类型的变量,然后才能进行具体操作。示例代码如下。
function addNumber<T>(target: T, num: number): T {
if (typeof target == "number") {
let result = target + num;
return (result as unknown) as T;
}
else if (target instanceof Array) {
target.push(3);
return target;
}
return target;
}
在类中使用泛型
类中也可以使用泛型。要声明泛型类,需要在类名称后使用尖括号来指定一个或多个泛型参数,具体语法如下。这些泛型参数可以作为类中各个函数参数的类型或返回值的类型,也可以作为类中各个属性的类型。当然,类中各个函数参数、返回值,以及各个属性的类型也可以为普通类型。
class 类名<T1,T2,T3,...> {
//属性
属性名称1: 属性类型;
属性名称2: 属性类型;
...
//构造函数
constructor(参数列表...) {
//构造实例对象时的初始化代码
}
//方法
方法名称1(参数列表...): 返回值类型 {
//方法代码块
}
方法名称2(参数列表...): 返回值类型 {
//方法代码块
}
...
}
例如,以下代码中声明了一个名为 Content
的泛型类,该泛型类拥有一个泛型参数 T
,在泛型类中定义了一个 contents
属性,其类型为 T
,然后定义了一个构造函数,该函数需要传入一个 T
类型的参数,并将该参数赋给 contents
属性,最后定义了一个 getContents()
方法,该方法的返回值类型为 T
,返回值为 contents
属性。
class Content<T> {
contents: T;
constructor(value: T) {
this.contents = value;
}
getContents(): T {
return this.contents;
}
}
此时就可以实例化泛型类。只需要为泛型参数 T
指定具体类型,就可以产生支持具体类型的类的实例。示例代码如下。
let a = new Content<string>("hello");
console.log(a.getContents()); //输出"hello"
let b = new Content<number>(1);
console.log(b.getContents()); //输出1
let c = new Content<{ x: boolean }>({ x: true });
console.log(c.getContents()); //输出{x:true}
类的成员分为静态成员和实例成员两种,泛型类的泛型参数只能用于实例成员,不能用于静态成员,否则将引起编译错误,示例代码如下。 |
class Content<T> {
//编译错误:静态成员不能引用类类型参数。ts(2302)
static contents: T;
}
泛型类型
除泛型函数和泛型类之外,泛型还可以用于一些类型声明的语句上,如函数签名、类型别名、接口等的类型声明。
例如,以下代码定义了一个名为 function1
的变量,它的类型为函数签名 <T>(arg:T) => T
,在为该变量赋值时,必须赋予满足该类型的函数。
let function1: <T>(arg: T) => T;
function1 = function <T>(arg: T) {
return arg;
}
接口也是一种类型声明,因此泛型也可以应用于接口。例如,以下代码定义了一个泛型接口 Content
,它拥有一个泛型参数 T
,接口中定义了一个属性 contents
,该属性是 T
类型。
interface Content<T> {
contents: T;
}
泛型接口可以被泛型类继承。例如,以下代码中的 HtmlContent
类继承了泛型接口 Content
,它拥有一个泛型参数 T
,在泛型类中定义了一个 contents
属性,该属性的类型为 T
,然后定义了一个构造函数,该函数需要传入一个 T
类型的参数,并将该参数赋给 contents
属性,最后定义了一个 getContents()
方法,该方法的返回值类型为 T
,返回值为 contents
属性。
class HtmlContent<T> implements Content<T>{
contents: T;
constructor(contents: T) {
this.contents = contents;
}
getContents() {
return this.contents;
}
}
类型别名也是一种类型声明,因此泛型也可以应用于类型别名。例如,以下代码定义了一个泛型类型别名 Content
,它拥有一个泛型参数 T
,接口中定义了一个属性 contents
,为 T
类型,然后声明了 3 个该泛型别名类型的变量,并赋予了相应的值。
type Content<T> = {
contents: T;
}
let a: Content<string> = { contents: "hello" };
let b: Content<number> = { contents: 1 };
let c: Content<boolean> = { contents: true };
除自定义泛型类型之外,还有一些在 TypeScript 中已有的并且可以直接使用的泛型类型,如泛型数组 Array<T>
,示例代码如下。
//Array<number>等同于number[]
let array1: Array<number> = [1,2,3];
//Array<string>等同于string[]
let array2: Array<string> = ["a","b","c"];
泛型约束
泛型是一种通用类型,它支持任意类型,但在某些情况下若需要限定泛型支持的类型范围,就需要使用泛型约束。泛型约束的语法如下。
<泛型参数 extends 具体类型>
例如,以下代码声明了一个 printLength()
函数,用于输出传入参数 arg
的 length
属性,但 arg
参数是泛型参数 T
类型的,该参数可能是任何类型,因此无法确定它是否具备 length
属性,这将引起编译错误。
function printLength<T>(arg: T) {
//编译错误:类型"T"上不存在属性"length"。ts(2339)
console.log(arg.length);
}
此时可以使用泛型约束,要求泛型参数 T
必须具有 length
属性。例如,以下代码声明了一个 Lengthwise
接口,它拥有一个 number
类型的 length
属性,在声明泛型函数 printLength()
时,使用 <T extends Lengthwise>
语句将泛型参数 T
限定为必须符合 Lengthwise
类型(或 Lengthwise
的子类型)。
interface Lengthwise {
length: number;
}
function printLength<T extends Lengthwise>(arg: T) {
console.log(arg.length);
}
在使用泛型函数 printLength()
时,为泛型参数 T
指定的具体类型必须具有 length
属性,以下代码均能正常编译、执行。
printLength<string>("abc");
printLength<number[]>([1, 2, 3]);
printLength<Lengthwise>({ length: 3 });
printLength<{ width: number, height: number, length: number }>({ length: 3, width: 1, height: 2 });
如果为泛型参数 T
指定的具体类型没有 length
属性,就会引起编译错误,示例代码如下。
//编译错误:类型"number"不满足约束"Lengthwise"。ts(2344)
printLength<number>(1);
//编译错误:类型"{ x: string; }"不满足约束"Lengthwise"。ts(2344)
printLength<{x:string}>({x:"hello"});
除指定具体类型之外,还可以使用 <泛型参数1 extends泛型参数2> 的语法,将泛型约束指定为某一参数必须符合其他泛型参数的类型(或为其他泛型参数的子类型)。例如,以下代码声明了一个 printArgs()
函数,它拥有两个泛型参数,其中 T2
必须符合 T1
的类型(或为 T1
的子类型)。
function printArgs<T1, T2 extends T1>(arg1: T1, arg2: T2) {
console.log(arg1);
console.log(arg2);
}
在调用该泛型函数时,T2
的具体类型必须满足 T1
。以下代码均能正常编译执行。
printArgs<{ x: number }, { x: number, y: number }>({ x: 1 }, { x: 1, y: 2 });
//1 | 2 | 3为字面量联合类型,它为number的子类型
printArgs<number, 1 | 2 | 3>(1, 1);
如果 T2
的具体类型不满足 T1
,则会引起编译错误。示例代码如下。
//编译错误:类型"{ y: number; }"不满足约束"{ x: number; }"。ts(2344)
printArgs<{ x: number }, { y: number }>({ x: 1 }, { y: 2 });
//编译错误:类型"number"不满足约束"string"。ts(2344)
printArgs<string, number>("a", 2);