一、泛型是什么
我们创造了一种可重用的组件,叫做泛型(Generics),用来处理不同类型的对象而并非单一类型的对象。它具有复用性和支持未来数据类型,泛型就是对类型进行编程
复用性
function id(arg: boolean): boolean {
return arg;
}
function id(arg: number): number {
return arg;
}
function id(arg: string): string {
return arg;
}
如果想输入什么,输出就是什么,在不使用泛型的情况下只能只用any
function identity(arg: any): any {
return arg;
}
但是如果我们使用any类型,在我们调用这个方法获得返回值后我们就失去了这个输出结果的数据类型。我们如果输入一个数字(number),那么能得到的就只有any类型。失去了类型保护和语法提示
因此,我们需要使用类型捕捉的方式来进行类型的获取。这样我们在获取返回值时也可以获取到返回值的类型。在这里,我们使用一种叫做类型变量(type variable)的特殊变量。这种变量专门处理变量的类型而不是变量的值。
使用泛型对上面的代码进行重构
T 是一个抽象类型,只有在调用的时候才确定它的值
function id<T>(arg: T): T {
return arg;
}
为了便于大家更好地理解上述的内容,我们来举个例子,在这个例子中,我们将一步步揭示泛型的作用。
首先定义一个类:
class People {
name!: string;
age!: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
再定义一个工厂函数:
function create(Constructor: { new (...args: any): any }) {
return new Constructor();
}
此时我们在调用工厂函数返回值得时候,其实已经失去了约束和提示,因为我们的参数设定的是any,返回值类型也是any
// function create(Constructor: new (...args: any) => any): any
create(People).wsy;
加入泛型,此时就能提示出类上有的属性
function create<T>(Constructor: { new (...args: any): T }) {
return new Constructor();
}
create(People);
对于刚接触 TypeScript 泛型的读者来说,首次看到
其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:
- K(Key):表示对象中的键类型;
- V(Value):表示对象中的值类型;
- E(Element):表示元素类型。
```typescript
function identity
(value: T, message: U) : T { console.log(message); return value; }
console.log(identity
<br />对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。下面我们来看张动图,直观地感受一下类型传递的过程:<br />
<a name="aCoVa"></a>
## 二、泛型接口
为了解决上面提到的问题,首先让我们创建一个用于的 identity 函数通用 Identities 接口:
```typescript
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"));
以上代码成功运行后,在控制台会输出以下结果:
{ value: 68, message: 'Semlinker' }
三、泛型类
在类中使用泛型也很简单,我们只需要在类名后面,使用
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!
我们在什么时候需要使用泛型呢?通常在决定是否使用泛型时,我们有以下两个参考标准:
- 当你的函数、接口或类将处理多种数据类型时;
- 当函数、接口或类在多个地方使用该数据类型时。
很有可能你没有办法保证在项目早期就使用泛型的组件,但是随着项目的发展,组件的功能通常会被扩展。这种增加的可扩展性最终很可能会满足上述两个条件,在这种情况下,引入泛型将比复制组件来满足一系列数据类型更干净。
我们将在本文的后面探讨更多满足这两个条件的用例。不过在这样做之前,让我们先介绍一下 Typescript 泛型提供的其他功能。
四、泛型约束
有时我们可能希望限制每个类型变量接受的类型数量,这就是泛型约束的作用。下面我们来举几个例子,介绍一下如何使用泛型约束。
4.1 确保属性存在
function identity<T extends { length: number }>(arg: T): T {
console.log(arg.length); // 可以获取length属性
return arg;
}
T extends { length: number } 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。之后,当我们使用不含有 length 属性的对象作为参数调用 identity 函数时,TypeScript 会提示相关的错误信息:
identity(1)
// Argument of type 'number' is not assignable to parameter of type '{ length: number; }'
4.2 约束类型
function sortChinese<T>(arr: Array<T>): T[] {
return arr.sort((a, b) => {
return a.localeCompare(b, 'zh-CN');
});
}
// Property 'localeCompare' does not exist on type 'T'
此时会提示类型“T”上不存在属性“localeCompare”
用extends约束T的类型
function sortChinese<T extends string>(arr: Array<T>): T[] {
return arr.sort((a, b) => {
return a.localeCompare(b, 'zh-CN');
});
}
4.3 检查对象上的键是否存在
泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 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]; 不同。
const a = {
name: 'Semlinker',
age: 18,
height: 18,
};
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
console.log(getProperty(a, 'name'));
可能会有疑问的是,如果按照下列的写法去写也能够约束key是T的属性
function getProperty<T>(obj: T, key: keyof T) {
return obj[key];
}
const a1 = getProperty(a, 'name');
console.log(a1);
若使用 keyof T 作为 key 的类型,那 obj[key] 就是 T[keyof T] 类型——这无疑不够精确
const a1: string | number
结论就是不够精准,泛型约束是必须的,是为了让 obj[key] 成为类型正确的表达式。
五、泛型参数默认类型
未指定默认值
function sortChinese<T>(arr: Array<T>): T[] {
return arr.sort((a, b) => {
return a.localeCompare(b, 'zh-CN');
});
}
sortChinese(1);
// 类型“number”的参数不能赋给类型“unknown[]”的参数。
指定了默认值
function sortChinese<T = string>(arr: Array<T>): T[] {
return arr.sort((a, b) => {
return a.localeCompare(b, 'zh-CN');
});
}
sortChinese(['1']);
// function sortChinese<string>(arr: string[]): string[]
如果添加了约束
function sortChinese<T extends string = string>(arr: Array<T>): T[] {
return arr.sort((a, b) => {
return a.localeCompare(b, 'zh-CN');
});
}
sortChinese(['1', 's']);
// function sortChinese<"1" | "s">(arr: ("1" | "s")[]): ("1" | "s")[]
六、泛型条件类型
在 TypeScript 2.8 中引入了条件类型,使得我们可以根据某些条件得到不同的类型,这里所说的条件是类型兼容性约束。尽管以上代码中使用了 extends 关键字,也不一定要强制满足继承关系,而是检查是否满足结构兼容性。
条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
T extends U ? X : Y
以上表达式的意思是:若 T 能够赋值给 U,那么类型是 X,否则为 Y。在条件类型表达式中,我们通常还会结合 infer 关键字,实现类型抽取:
type GetFirst<Arr extends unknown[]> = Arr extends [infer First, ...unknown[]]
? First
: never;
type GetFirstResult = GetFirst<[1, 2, 3]>; // 1