一、泛型是什么
设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
为了便于大家更好地理解上述的内容,我们来举个例子,在这个例子中,我们将一步步揭示泛型的作用。首先我们来定义一个通用的 identity
函数,该函数接收一个参数并直接返回它:
function identity (value) {
return value;
}
console.log(identity(1)) //输出 1
现在,我们将identity
函数做适当的调整,以支持 TypeScript 的 Number
类型的参数:
function identity (value: Number) : Number {
return value; //返回值大小不是目的,而是返回值的类型 return value++ 也可以演示这个例子
}
console.log(identity(1)) //输出 1
console.log(identity('1')) // 编译失败,字符串类型不符合Number类型检查
从上面的代码,我们能读懂如下的约束:
1.参数必须是Number类型
2.返回值必须是Number类型
3.参数类型和返回类型相同
这里identity
的是以Number
类型作为例子,但该函数并不是可扩展或通用的,简单来说,如果我想定义一个函数,参数类型和返回类型相同,那么传入一个string
类型参数时,返回也必须是string
类型,针对其他基本类型
,甚至object类型
,也可以满足同样的要求的话,我们该如何做呢。
我们确实可以把 Number
换成any
,我们失去了定义应该返回哪种类型的能力,并且在这个过程中使编译器失去了类型保护的作用。
function identity (value: any) : any {
return value.toString(); //转化为string类型
}
console.log(identity(1)) //输入是number类型, 输出是string类型 "1"
上面的代码,并不能达到限制入参和返回值类型相同的目标。
我们的目标是让identity
函数可以适用于任何特定的类型,为了实现这个目标,我们可以使用泛型来解决这个问题,具体实现方式如下:
function identity <T>(value: T) : T {
return value; //此处代码必须返回和输入参数类型相同的,否则编译错误
}
console.log(identity<Number>(1)) // 输入number,输出也是number
对于刚接触 TypeScript 泛型的读者来说,首次看到 <T>
语法会感到陌生。但这没什么可担心的,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。
参考上面的图片,当我们调用identity<Number>(1)
,Number
类型就像参数 1
一样,它将在出现 T
的任何位置填充该类型。图中 <T>
内部的 T
被称为类型变量,它是我们希望传递给identity
函数的类型占位符,同时它被分配给value
参数用来代替它的类型:此时 T
充当的是类型,而不是特定的 Number
类型。
其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量U
,用于扩展我们定义的 identity
函数:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity<Number, string>(68, "Semlinker"));
除了为类型变量显式设定值之外,一种更常见的做法是使编译器自动选择这些类型,从而使代码更简洁。我们可以完全省略尖括号
,比如:
function identity <T, U>(value: T, message: U) : T {
console.log(message);
return value;
}
console.log(identity(68, "Semlinker")); //省略了<Number, string>
对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。
相比之前定义的identity
函数,新的identity
函数增加了一个类型变量 U,但该函数的返回类型我们仍然使用T
。如果我们想要返回两种类型的对象该怎么办呢?针对这个问题,我们有多种方案,其中一种就是使用元组,即为元组设置通用的类型:
function identity <T, U>(value: T, message: U) : [T, U] {
return [value, message];
}
虽然使用元组解决了上述的问题,但有没有其它更好的方案呢?答案是有的,你可以使用泛型接口。
二、泛型接口
为了解决上面提到的问题,首先让我们创建一个用于的identity
函数通用 Identities 接口:
interface Identities<V, M> {
value: V,
message: M
}
在上述的 Identities
接口中,我们引入了类型变量 V
和M
,来进一步说明有效的字母都可以用于表示类型变量,之后我们就可以将Identities
接口作为identity
函数的返回类型:`
function identity<T, U> (value: T, message: U): Identities<T, U> {
console.log(value + ": " + typeof (value));
console.log(message + ": " + typeof (message));
let identities: Identities<T, U> = {
value,
message
};
return identities;
}
console.log(identity(68, "Semlinker"));
以上代码成功运行后,在控制台会输出以下结果:
68: number
Semlinker: string
{value: 68, message: "Semlinker"}
泛型除了可以应用在函数和接口之外,它也可以应用在类中,下面我们就来看一下在类中如何使用泛型。
三、泛型类
在类中使用泛型也很简单,我们只需要在类名后面
,使用 <T, ...>
的语法定义任意多个类型变量,具体示例如下:
interface GenericInterface<U> {
value: U
getIdentity: () => U
}
class IdentityClass<T> implements GenericInterface<T> {
value: T
constructor(value: T) {
this.value = value
}
getIdentity(): T {
return this.value
}
}
const myNumberClass = new IdentityClass<Number>(68);
console.log(myNumberClass.getIdentity()); // 68
const myStringClass = new IdentityClass<string>("Semlinker!");
console.log(myStringClass.getIdentity()); // Semlinker!
接下来我们以实例化 myNumberClass
为例,来分析一下其调用过程:
在实例化 IdentityClass
对象时,我们传入Number
类型和构造函数参数值 68;
之后在IdentityClass
类中,类型变量T
的值变成Number
类型;
IdentityClass
类实现了GenericInterface<T>
,而此时T
表示 Number
类型,因此等价于该类实现了 GenericInterface<Number>
接口;
而对于 GenericInterface<U>
接口来说,类型变量 U
也变成了Number
。这里我有意使用不同的变量名,以表明类型值沿链向上传播,且与变量名无关。
泛型类可确保在整个类中一致地使用指定的数据类型。比如,你可能已经注意到在使用 Typescript 的 React 项目中使用了以下约定:
type Props = {
className?: string
...
};
type State = {
submitted?: bool
...
};
class MyComponent extends React.Component<Props, State> {
...
}
在以上代码中,我们将泛型与 React 组件一起使用,以确保组件的 props 和 state 是类型安全的。
相信看到这里一些读者会有疑问,我们在什么时候需要使用泛型呢?通常在决定是否使用泛型时,我们有以下两个参考标准:
当你的函数、接口或类将处理多种数据类型时;
当函数、接口或类在多个地方使用该数据类型时。
很有可能你没有办法保证在项目早期就使用泛型的组件,但是随着项目的发展,组件的功能通常会被扩展。这种增加的可扩展性最终很可能会满足上述两个条件,在这种情况下,引入泛型将比复制组件来满足一系列数据类型更干净。
我们将在本文的后面探讨更多满足这两个条件的用例。不过在这样做之前,让我们先介绍一下 Typescript 泛型提供的其他功能。
四、泛型约束
有时我们可能希望限制每个类型变量接受的类型数量,这就是泛型约束的作用。下面我们来举几个例子,介绍一下如何使用泛型约束。
4.1 确保属性存在
有时候,我们希望类型变量对应的类型上存在某些属性。这时,除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。
一个很好的例子是在处理字符串或数组时,我们会假设 length 属性是可用的。让我们再次使用 identity 函数并尝试输出参数的长度:
function identity<T>(arg: T): T {
console.log(arg.length); // Error
return arg;
}
在这种情况下,编译器将不会知道 T 确实含有length
属性,尤其是在可以将任何类型赋给类型变量 T 的情况下。我们需要做的就是让类型变量 extends
一个含有我们所需属性的接口,比如这样:
interface Length {
length: number;
}
function identity<T extends Length>(arg: T): T {
console.log(arg.length); // 可以获取length属性
return arg;
}
T extends Length
用于告诉编译器,我们支持已经实现 Length
接口的任何类型。之后,当我们使用不含有length
属性的对象作为参数调用 identity 函数时,TypeScript 会提示相关的错误信息:
identity(68); // Error
// Argument of type '68' is not assignable to parameter of type 'Length'.(2345)
此外,我们还可以使用 , 号来分隔多种约束类型,比如:<T extends Length, Type2, Type3>
。而对于上述的 length
属性问题来说,如果我们显式地将变量设置为数组类型,也可以解决该问题,具体方式如下:
function identity<T>(arg: T[]): T[] { //T[],表明这个是数组形式的形参,数组一定有length方法
console.log(arg.length);
return arg;
}
// or
function identity<T>(arg: Array<T>): Array<T> { //同样, Array一定有 length方法
console.log(arg.length);
return arg;
}
4.2 检查对象上的键是否存在
泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 keyof
操作符,keyof
操作符是在 TypeScript 2.1
版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。 “耳听为虚,眼见为实”,我们来举个keyof
的使用示例:
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
type K3 = keyof { [x: string]: Person }; // string | number
通过 keyof
操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends
约束,即限制输入的属性名包含在 keyof
返回的联合类型中。具体的使用方式如下:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
在以上的 getProperty
函数中,我们通过 K extends keyof T
确保参数 key
一定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单调用let value = obj[key];
不同。
下面我们来看一下如何使用 getProperty 函数:
enum Difficulty {
Easy,
Intermediate,
Hard
}
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let tsInfo = {
name: "Typescript",
supersetOf: "Javascript",
difficulty: Difficulty.Intermediate
}
let difficulty: Difficulty =
getProperty(tsInfo, 'difficulty'); // OK
let supersetOf: string =
getProperty(tsInfo, 'superset_of'); // Error,应该是supersetOf
在以上示例中,对于 getProperty(tsInfo, ‘superset_of’) 这个表达式,TypeScript 编译器会提示以下错误信息:
Argument of type ‘“superset_of”’ is not assignable to parameter of type
’"difficulty"
|"name"
|"supersetOf"
'.(2345)
很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。接下来,我们来介绍一下泛型参数默认类型。
五、泛型参数默认类型
在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型
。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。
泛型参数默认类型与普通函数默认值类似,对应的语法很简单,即 <T=Default Type>
,对应的使用示例如下:
interface A<T=string> {
name: T;
}
const strA: A = { name: "Semlinker" };
const numB: A<number> = { name: 101 };
const numB: A = { name: 101 }; //编译错误,Type '{ name: number; }' is not assignable to type 'A<string>'
六、泛型条件类型
在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。尽管以上代码中使用了 extends 关键字,也不一定要强制满足继承关系,而是检查是否满足结构兼容性。
条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
T extends U ? X : Y
以上表达式的意思是:若 T 能够赋值给 U,那么类型是 X,否则为 Y。在条件类型表达式中,我们通常还会结合 infer 关键字,实现类型抽取:
interface Dictionary<T = any> {
[key: string]: T;
}
type StrDict = Dictionary<string>
type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string
在上面示例中,当类型T
满足 T extends Dictionary
约束时,我们会使用 infer
关键字声明了一个类型变量V
,并返回该类型,否则返回 never
类型。
在 TypeScript 中,never 类型表示的是那些永不存在的值的类型。 例如, never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型。
另外,需要注意的是,没有类型是 never 的子类型或可以赋值给 never 类型(除了 never 本身之外)。 即使 any 也不可以赋值给 never。
除了上述的应用外,利用条件类型和 infer 关键字,我们还可以方便地实现获取 Promise 对象的返回值类型,比如:
async function stringPromise() {
return "Hello, Semlinker!";
}
interface Person {
name: string;
age: number;
}
async function personPromise() {
return { name: "Semlinker", age: 30 } as Person;
}
type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;
type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person