从 JavaScript 到 TypeScript - 泛型

TypeScript 为 JavaScriopt 带来了强类型特性,这就意味着限制了类型的自由度。同一段程序,为了适应不一样的类型,就可能须要写不一样的处理函数——并且这些处理函数中全部逻辑彻底相同,惟一不一样的就是类型——这严重违反抽象和复用代码的原则。typescript

一个小实例

咱们来模拟一个场景:某个服务提供了一些不一样类型的数据,咱们须要先经过一个中间件对这些数据进行一个基本的处理(好比验证,容错等),再对其进行使用。那么用 JavaScript 来写应该是这样的编程

JavaScript 源码

// 模拟服务,提供不一样的数据。这里模拟了一个字符串和一个数值
var service = {
    getStringValue: function() {
        return "a string value";
    },
    getNumberValue: function() {
        return 20;
    }
};

// 处理数据的中间件。这里用 log 来模拟处理,直接返回数据看成处理后的数据
function middleware(value) {
    console.log(value);
    return value;
}

// JS 中对于类型并不关心,因此这里没什么问题
var sValue = middleware(service.getStringValue());
var nValue = middleware(service.getNumberValue());

改写成 TypeScript

先来看看对服务的改写,TypeScript 版的服务有返回类型:segmentfault

const service = {
    getStringValue(): string {
        return "a string value";
    },

    getNumberValue(): number {
        return 20;
    }
};

为了保证在对 sValuenValue 的后续操做中类型检查有效,它们也会有类型(若是 middleware 类型定义得当,能够推导,这里咱们先显示定义其类型)数组

const sValue: string = middleware(service.getStringValue());
const nValue: number = middleware(service.getNumberValue());

如今的问题是 middleware 要怎么样定义才既可能返回 string,又可能返回 number,并且还能被类型检查正确推导出来?dom

第 1 个办法,用 any

function middleware(value: any): any {
    console.log(value);
    return value;
}

是的,这个办法能够检查经过。但它的问题在于 middleware 内部失去了类型检查,在后在对 sValuenValue 赋值的时候,也只是看成类型没有问题。简单的说,是有“伪装”没问题。模块化

第 2 个办法,多个 middleware

function middleware1(value: string): string { ... }
function middleware2(value: number): number { ... }

固然也能够用 TypeScript 的重载(overload)来实现函数

function middleware(value: string): string;
function middleware(value: number): number;
function middleware(value: any): any {
    // 实现同样没有严格的类型检查
}

这种方法最主要的一个问题是……若是我有 10 种类型的数据,就须要定义 10 个函数(或重载),那 20 个,200 个呢……工具

正解:使用泛型(Generic)

如今咱们切入正题,用泛型来解决这个问题。那么这就须要解释一下什么是泛型了:泛型就是指定一个表示类型的变量,用它来代替某个实际的类型用于编程,然后经过实际调用时传入或推导的类型来对其进行替换,以达到一段使用泛型程序能够实际适应不一样类型的目的。this

虽然这个解释已经很接地气了,可是理解起来仍是不如一个实例来得容易。咱们来看看 middleware 的泛型实现是怎么样的spa

function middleware<T>(value: T): T {
    console.log(value);
    return value;
}

middleware 后面紧接的 <T> 表示声明一个表示类型的变量,Value: T 表示声明参数是 T 类型的,后面的 : T 表示返回值也是 T 类型的。那么在调用 middlewre(getStringValue()) 的时候,因为参数推导出来是 string 类型,因此这个时候 T 表明了 string,所以此时 middleware 的返回类型也就是 string;而对于 middleware(getNumberValue()) 调用来讲,这里的 T 表示了 number

咱们直接从 VSCode 的提示能够看出来,对于 middleware<T>() 调用,TypeScript 能够推导出参数类型和返回值类型:

clipboard.png

咱们也能够在调用的时候,小括号前显示指定 T 代替的类型,好比 mdiddleware<string>(...),不过若是指定的类型与推导的类型有冲突,就会提示错误:

clipboard.png

泛型类

前面已经解释了“泛型”这个概念。示例中泛型的用法咱们称之为“泛型函数”。不过泛型更普遍的用法是用于“泛型类”——即在声明类的时候声明泛型,那么在类的整个个做用域范围内均可以使用声明的泛型类型。

相信你们都已经对数组有所了解,好比 string[] 表示字符串数组类型。其实在早期的 TypeScript 版本中没有这种数组类型表示,而是采用实例化的泛型 Array<string> 来表示的,如今仍然可使用这方式来表示数组。

除此以外,TypeScript 中还有一个很经常使用的泛型类,Promise<T>。由于 Promise 每每是带数据的,因此经过 Promise<T> 这种泛型定义的形式,能够表示一个 Promise 所带数据的类型。好比下图就能够看出,TypeScript 能正确推导出 n 的类型是 number

clipboard.png

因此,泛型类其实多数时候是应用于容器类。假设咱们须要实现一个 FilteredList,咱们能够向其中 add()(添加) 任意数据,可是它在添加的时候会自动过滤掉不符合条件的一些,最终经过 get all() 输出全部符合条件的数据(数组)。而过滤条件在构造对象的时候,以函数或 Lambda 表达式提供。

// 声明泛型类,类型变量为 T
class FilteredList<T> {
    // 声明过滤器是以 T 为参数类型,返回 boolean 的函数表达式
    filter: (v: T) => boolean;
    // 声明数据是 T 数组类型
    data: T[];
    constructor(filter: (v: T) => boolean) {
        this.filter = filter;
    }

    add(value: T) {
        if (this.filter(value)) {
            this.data.push(value);
        }
    }

    get all(): T[] {
        return this.data;
    }
}

// 处理 string 类型的 FilteredList
const validStrings = new FilteredList<string>(s => !s);

// 处理 number 类型的 FilteredList
const positiveNumber  = new FilteredList<number>(n => n > 0);

甚至还能够把 (v: T) => boolean 声明为一个类型,以便复用

type Predicate<T> = (v: T) => boolean;

class FilteredList<T> {
    filter: Predicate<T>;
    data: T[];
    constructor(filter: Predicate<T>) { ... }
    add(value: T) { ... }
    get all(): T[] { ... }
}

固然类型变量也不必定非得叫 T,也能够叫 TValue 或别的什么,可是通常建议以大写的 T 做为前缀,采用 Pascal 命名规则,方便识别。还有一些常见的指代,好比 TKey 表示键类型,TValue 表示值类型等(经常使用于映射表这类容器定义)。

泛型约束

有了泛型以后,一个函数或容器类能处理的类型一会儿扩到了无限大,彷佛有点失控的感受。因此这里又产生了一个约束的概念。咱们能够声明对类型参数进行约束。

好比,咱们有 IAnimal 这样一个接口,而后写一个 run 工具函数,它可让动物跑起来,并且它会返回这个动物实例自己(以便链式调用)。先来定义类型

interface IAnimal {
    run(): void;
}

class Dog implements IAnimal {
    run(): void {
        console.log("Dog is running");
    }
}

第 1 种 run 定义,使用接口或基类类型

function run(animal: IAnimal): IAnimal {
    animal.run();
    return animal;
}

const dog = run(new Dog());    // dog: IAnimal

这种定义的缺点是 dog 被推导成 IAnimal 类型,固然能够经过强制声明为 const dog: Dog 来指定其类型,可是谁知道 run() 返回的是 Dog 而不是 Cat 呢。

第 2 种 run 定义,使用泛型(无约束)

function run<TAnimal>(animal: TAnimal): TAnimal {
    animal.run();   // 'run' does not exist on type 'TAnimal'
    return animal;
}

采用这种定义,dog 能够推导正确。不过因为 TAnimal 在这里只是个变量,能够表明任意类型,因此它并不能保证拥有 run() 方法可供调用。

第 3 种 run 定义,使用泛型约束

正解是使用泛型约束,将 TAnimal 约束为实现了 IAnimal。这须要在定义类型变量的使用使用 extends 来约束:

function run<TAnimal extends IAnimal>(animal: TAnimal): TAnimal {
    animal.run();   // it's ok
    return animal;
}

注意这里的语法,<TAnimal extends IAnimal>,虽然 IAnimal 是个接口,但这里不是在实现接口,extends 表示约束关系,而非继承。它表示 extends 左边的类型变量实现了右边的类型,或者是右边类型的子孙类,或者就是右边的那个类型。简单的说,就是左边类型的实例能够赋值给右边类型的变量。

约束为类型

有时候咱们但愿传入某个工具方法的参数是一个类型,这样就能够经过 new 来生成对象。这在 TypeScript 中一般是使用构造函数来约束的,好比

function create<T extends IAnimal>(type: { new(): T }) {
    return new type();
}

const dog = create(Dog);

这里约束了 create 能够建立动物的实例。若是不加 extends IAnimal,那么这个 create 能够建立任何类型的实例。

多个类型变量

在使用泛型的时候,固然不会限制只使用一个类型变量,咱们可使用多个,好比能够这样定义一个 Pair

class Pair<TKey, TValue> {
    private _key: TKey;
    private _value: TValue;
    constructor(key: TKey, value: TValue) {
        this._key = key;
        this._value = value;
    }

    get key() { return this._key; }
    get value() { return this._value; }
}

其它应用

本身定义泛型结构(泛型类或泛型函数)一般只会在写比较复杂的应用时发生。可是使用已定义好的泛型是极其常见的,上面已经提到了两个常见的泛型定义,T[]/Array<T>Promise<T>,除此以外,还有 ES6 的 SetMap 对应于 TypeScript 的泛型定义 Set<T>Map<TK, TV>。另外,泛型还经常使用于 Generator 和 Iterable/Iterator:

// 产生 n 个随机整数
function* randomInt(n): Iterable<number> {
    for (let i = 0; i < n; i++) {
        yield ~~(Math.random() * Number.MAX_SAFE_INTEGER);
    }
}

for (let n of randomInt(10)) {
    console.log(n);
}

扩展阅读


关注做者的公众号“边城客栈” →

相关文章
相关标签/搜索