TypeScript 学习总结之 泛型


TypeScript

泛型

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

1
2
3
4
5
6
// T : 泛型变量 | 类型变量
function identity<T>(arg: T): T {
return arg;
}
identity(123)
identity('123')

代码中 T 代表 「Type」,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。

由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

1
2
3
4
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}

其实并不是只能定义一个类型变量,我们可以引入希望定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:

1
2
3
4
5
6
function identity<T, U>(value: T, message: U): T {
console.log(message);
return value;
}

console.log(identity(666, "jack is cool!"));

泛型类型 && 泛型接口

泛型类型不同的注解方式:

  1. 函数泛型的注解方式
  2. 对象字面量的方式来定义泛型类型
  3. 泛型接口的定义方式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //  泛型类型的不同方式
    function identity<T>(arg: T): T {
    return arg;
    }

    // 1. 函数泛型的注解方式:
    let a: <T>(arg: T) => T = identity

    // 2. 对象字面量的方式来定义泛型类型
    let b: { <T>(arg: T): T } = identity

    // 3. 泛型接口的定义方式
    interface IdentityInterface {
    <T>(arg: T): T
    }
    let c: IdentityInterface = identity

泛型类 & 泛型约束

泛型类

泛型类看上去与泛型接口差不多,我们只需要在类名后面,使用 <T, …> 的语法定义任意多个类型变量,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 泛型类
class MinClass<T>{
public list: T[] = []
add(num: T) {
this.list.push(num)
}
min(): T {
let minNum = this.list[0]
for (let i = 0; i < this.list.length; i++) {
if (minNum > this.list[i]) {
minNum = this.list[i]
}
}
return minNum
}
}

我们在什么时候需要使用泛型呢?通常在决定是否使用泛型时,我们有以下两个参考标准:

  • 当你的函数、接口或类将处理多种数据类型时;
  • 当函数、接口或类在多个地方使用该数据类型时。

泛型约束

有时我们可能希望限制每个类型变量接受的类型数量,这就是泛型约束的作用。

以官方文档例子为例:

我们需要去定义一个接口来描述约束条件。

创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

1
2
3
4
5
6
7
8
interface LengthInterface {
length: number
}

function loggingIdentity<T extends LengthInterface>(arg: T): T {
console.log(arg.length);
return arg;
}

其中,泛型约束用的是: extends 继承接口的方式(不一定非要是接口)T extends LengthInterface 用于告诉编译器,我们支持已经实现 Length 接口的任何类型

另外, 泛型约束并不一定用接口方式, 比如 我们可以把以上代码 接口 换成 类型别名 ,
如下例子:
// 以类型别名的方式依然可以

1
2
3
4
5
6
type LengthType = string

function loggingIdentity<T extends LengthType>(arg: T): T {
console.log(arg.length);
return arg;
}

keyof 操作符

keyof操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

1
2
3
4
5
6
7
8
9
10
11
// 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

可以看出, k1,k2,k3其实是键名

在泛型约束中使用类型参数

当我们理解了 keyof 操作符时, 看下面代码就很好理解了

1
2
3
4
5
6
7
8
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

**而 K 就是指 ‘a’ , ‘b’ , ‘c’ , ‘d’ **

K

很明显通过使用泛型约束,在编译阶段我们就可以提前发现错误,大大提高了程序的健壮性和稳定性。

多重泛型约束 & 交叉类型

交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{}; // 断言, 表示 result 包含 T U 俩种类型
for (let id in first) {
(<any>result)[id] = (<any>first)[id]; // 将 first 中所有属性 给 result
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
// 将 second 中所有属性 给 result, 前提是result 没有该属性时
}
}
return result; // 返回的 result 就有 first 和 second 的所有属性
}


class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();

再举个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Sentence {
content: string
title: string
}

interface Music {
url: string
}

class Test<T extends Sentence & Music>{
props: T
constructor(public arg: T) {
this.props = arg
}

info() {
return {
// 这里可以 this.props.xxx 的原因是因为 arg 符合 T类型, 而T 又继承了上面俩个接口的属性。
content: this.props.content,
title: this.props.title,
url: this.props.url
}
}
}

泛型中的类类型

泛型中的类类型 目的是 约束或者更好的推论

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class BeeKeeper {
hasMask: boolean;
}

class ZooKeeper {
nametag: string;
}

class Animal {
numLegs: number;
}

class Bee extends Animal {
keeper: BeeKeeper;
}

class Lion extends Animal {
keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}

createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!