TypeScript 学习总结之 泛型

1,227 阅读4分钟

TypeScript

泛型

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

// T : 泛型变量 | 类型变量
function identity<T>(arg: T): T {
  return arg;
}
identity(123)
identity('123')

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

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

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

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

function identity<T, U>(value: T, message: U): T {
  console.log(message);
  return value;
}

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

泛型类型 && 泛型接口

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

  1. 函数泛型的注解方式
  2. 对象字面量的方式来定义泛型类型
  3. 泛型接口的定义方式
//  泛型类型的不同方式
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, ...> 的语法定义任意多个类型变量,具体示例如下:

// 泛型类
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关键字来实现约束:

interface LengthInterface {
  length: number
}

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

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

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

type LengthType = string

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

keyof 操作符

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

// 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 操作符时, 看下面代码就很好理解了

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

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

多重泛型约束 & 交叉类型

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


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();

再举个简单例子:

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
    }
  }
}

泛型中的类类型

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

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!