一、泛型

泛型在诸多高级语言中都存在,对于设计可充用的类或者组件都有非常大的作用。

比如下面一个 add 函数,只能接受 number 的参数并且返回 number 类型,如果需要 string 类型,则需要再写一个新函数。

function add(x: number, y: number) : number {
    return x + y;
}

当然也可以采用 any 类型,比如:

function add(x: any, y: any): any{
    return x + y;
}

但是这样子会导致参数类型和返回类型可能不一致的情况,这和我们的初衷是不相符的,解决这个问题可以使用类型变量。

类型变量 只用来表示类型而不是值,因此类型变量是一种特殊的变量。

function add<T>(x: T) :T {
    return x;
}

其中 add 这个函数称为泛型,适用于多个类型,但是不会和 any 一样丢失当前类型的控制。

使用泛型可以传入所有的参数,包括类型参数

let output = add<number>(12);

上面声明了类型是 <number> (注意声明方式),并且传入了一个 number 值,除此之外也可以通过类型推导自动确定 <T> 的类型。

let output = add(12);

如果主动声明了类型,则传入的参数需要和声明的类型一致:

1.png

我们没必要使用尖括号(<>)来明确地传入类型

编译器可以查看 参数 的值,然后把 T 设置为它的类型。
类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

二、使用泛型变量

上面只是简单的说了下泛型变量的定义,但使用泛型变量的时候会遇到很多的问题。

比如获取一个数组的长度:

function getLen<T>(arg: T): number {
    return arg.length;
}

2.png

上面代码是有问题的,报错如上,因为编译器无法推导 arg 是否拥有 .length 属性。

解决这种问题的方式是需要将参数声明为一个数组,但是数组内元素的类型我们并不关心,使用泛型。

function getLen<T>(arg: T[]): number {
 return arg.length;
}

除了上面这种方式之外,也可以像 Java 一样去声明这个泛型:

function getLen<T>(arg: Array<T>):number {
    return arg.length;
}

三、泛型类型

泛型函数的类型和非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样。

function getLen<T>(arg: Array<T>): number {
    return arg.length;
}

let getLen2:<T>(arg: Array<T>) => number = getLen;

可以使用不同的泛型参数名,只要同一个函数中在数量上和使用方式上能对应上就可以。

function getLen<U>(arg: Array<U>): number {
    return arg.length;
}

let getLen2:<T>(arg: Array<T>) => number = getLen;

泛型接口

首先声明一个接口:

interface GenericIdentityFn  {
    <T>(arg:T): T
}

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;

上面接口 GenericIdentityFn 声明了一个属性是 arg,通过泛型声明了类型

但是更多的时候我们可能希望通过 GenericIdentityFn<T> 的形式去使用这个泛型接口,直接在 接口层就控制类型。

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T> (arg: T) : T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

myIdentity('string'); // 报错,因为已经声明了 number 类型

3.png

除了泛型接口,还可以创建泛型类,但是无法创建泛型枚举和泛型命名空间。

四、泛型类

泛型类看上去与泛型接口差不多。 泛型类使用( <>)括起泛型类型,跟在类名后面。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();

myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

类有两部分:静态部分实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型

五、泛型约束

之前直接获取参数长度的泛型函数存在问题,可能会编译不通过:

function getLen<T>(arg: T):T {
    return arg.length; // 报错
}

这个时候除了主动声明他是个数组外,我们还可能需要使用 string 类型,泛型约束可以实现对某种类型的泛型约束。

使用的方法也是鸭子鉴别法。

interface Lengthwise {
    length: number;
}
function getLen <T extends Lengthwise>(arg: T):number {
    return arg.length;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

getLen(3);  // Error, number doesn't have a .length property

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

你可以声明一个类型参数,且它被另一个类型参数所约束。

比如,想要用属性名从对象里获取这个属性。 并且确保这个属性存在于对象 obj 上,因此我们需要在这两个类型之间使用约束。

function getProperty(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'.

在泛型里使用类类型

在TypeScript使用泛型创建工厂函数时,需要引用构造函数的类类型

function create<T>(c: {new(): T; }): T {
    return new c();
}

一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。

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!