你好,游客 登录
背景:
阅读新闻

从 JavaScript 到 TypeScript - 泛型

[日期:2017-08-22] 来源:SegmentFault   作者: [字体: ]

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

  一个小实例

  我们来模拟一个场景:某个服务提供了一些不同类型的数据,我们需要先通过一个中间件对这些数据进行一个基本的处理(比如验证,容错等),再对其进行使用。那么用 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 版的服务有返回类型:

  const service = {

  getStringValue(): string {

  return "a string value";

  },

  getNumberValue(): number {

  return 20;

  }

  };

  为了保证在对 sValue 和 nValue 的后续操作中类型检查有效,它们也会有类型(如果 middleware 类型定义得当,可以推导,这里我们先显示定义其类型)

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

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

  现在的问题是 middleware 要怎么样定义才能即可能返回 string ,又可能返回 number ,而且还能被类型检查正确推导出来?

  第 1 个办法,用 any

  function middleware(value: any): any {

  console.log(value);

  return value;

  }

  是的,这个办法可以检查通过。但它的问题在于 middleware 内部失去了类型检查,在后在对 sValue 和 nValue 赋值的时候,也只是当作类型没有问题。简单的说,是有 “假装” 没问题。

  第 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)

  现在我们切入正题,用泛型来解决这个问题。那么这就需要解释一下什么是泛型了:泛型就是指定一个表示类型的变量,用它来代替某个实际的类型用于编程,而后通过实际调用时传入或推导的类型来对其进行替换,以达到一段使用泛型程序可以实际适应不同类型的目的。

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

  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 可以推导出参数类型和返回值类型:

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

  泛型类

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

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

  除此之外,TypeScript 中还有一个很常用的泛型类, Promise<T> 。因为 Promise 往往是带数据的,所以通过 Promise<T> 这种泛型定义的形式,可以表示一个 Promise 所带数据的类型。比如下图就可以看出,TypeScript 能正确推导出 n 的类型是 number :

  所以,泛型类其实多数时候是应用于容器类。假设我们需要实现一个 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 的 Set 和 Map 对应于 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);

  }

  扩展阅读

  从 JavaScript 到 TypeScript - 模块化和构建

  从 JavaScript 到 TypeScript - 声明类型

收藏 推荐 打印 | 录入:Cstor | 阅读:
本文评论   查看全部评论 (0)
表情: 表情 姓名: 字数
点评:
       
评论声明
  • 尊重网上道德,遵守中华人民共和国的各项有关法律法规
  • 承担一切因您的行为而直接或间接导致的民事或刑事法律责任
  • 本站管理人员有权保留或删除其管辖留言中的任意内容
  • 本站有权在网站内转载或引用您的评论
  • 参与本评论即表明您已经阅读并接受上述条款