泛型 Generics - 图1Typescript中泛型的实现让我们能够将一系列类型传递给组件,为代码增加了额外的抽象层和可重用性。在Typescript中,泛型可以应用于函数、接口和类。

泛型: 抽象类型的能力

Generic Function

方法identity(),输入什么类型的参数,返回什么类型的参数:

  1. function identity<T>(arg: T): T {
  2. return arg;
  3. }

T是一个类型的占位符,放在尖括号<>中,代表参数 arg 的类型。

function identities<T, U> (arg1: T, arg2: U): [T, U] {
   return [arg1, arg2];
}

方法 identities() 支持传递两种类型的泛型,并返回一个元组类型,类型T和类型U。

然而,你可能希望一个特定的interface,来代替元组的写法,使代码更已读。

Generic Interfaces

interface Identities<V, W> {
   id1: V,
   id2: W
}
function identities<T, U> (arg1: T, arg2: U): Identities<T, U> {
   console.log(arg1 + ": " + typeof (arg1));
   console.log(arg2 + ": " + typeof (arg2));
   let identities: Identities<T, U> = {
    id1: arg1,
    id2: arg2
  };
  return identities;
}

现在我们对identity()做法的是将类型TU传递给函数和Identities接口,允许我们根据实参类型定义返回类型。

Generic Classes

我们还可以在类属性和方法上使类泛型。Generic Classes 确保在整个类中一致地使用指定的数据类型。

React Typescript 项目中,你可能会很熟悉下面的用法:

type Props = {
   className?: string
   ...
};
type State = {
   submitted?: bool
   ...
};
class MyComponent extends React.Component<Props, State> {
   ...
}

在这里我们使用泛型来确保组件的状态和属性类型安全。

下面的类中,可以处理多种类型:

class Programmer<T> {
  private languageName: string;
  private languageInfo: T;

  constructor(lang: string) {
    this.languageName = lang;
  }
  ...
}    
let programmer1 = new Programmer<Language.Typescript>("Typescript");
let programmer2 = new Programmer<Language.Rust>("Rust");

类型参数推理(type argument inference)的说明

上面的语法中,我们实例化一个类,用的是这样的语法:

let myObj = new className<Type>("args");

因为对于实例化类,编译器无法猜测我们要分配的类型。然而,对于函数,编译器可以猜测泛型是哪种类型。我们还是以 identities 为例:

调用方法,明确传递 string 和 number 给 T 和 U:

let result = identities<string, number>("argument 1", 100);

然而,更常见的做法是让编译器自动获取这些类型,使代码更清晰。我们可以完全省略尖括号,只写下面的语句:

let result = identities("argument 1", 100);

Caveat: 如果我们有一个没有参数的泛型返回类型,编译器将需要我们显式地定义类型。

什么时候使用泛型

通常在决定是否使用泛型时,我们应该满足两个标准:

  1. 当 function, interface 或 class 要处理各种数据类型时;
  2. 当 function, interface 或 class 在几个地方使用该数据类型。

很有可能在项目的早期,您没有一个使用泛型的组件。但随着项目的发展,组件的能力通常会扩大。这种增加的可扩展性最终可能会很好地遵守上述两个标准,在这种情况下,引入泛型将是一种更干净的替代方法,而不是仅仅为了满足一系列数据类型而复制组件。

泛型约束(Generic Constraints)

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

使用约束来确保类型的属性

例如,我们在处理字符串或数组时,.length属性是可用的,但泛型T不具有该属性,我们可以将泛型继承一个接口并含有必须的属性length:

interface Length {
    length: number;
}

function identity<T extends Length>(arg: T): T {
   // length property can now be called
   console.log(arg.length);
   return arg;
}

Note: 我们也可以将基础的类型用逗号分隔,例如:<T extends Length, Type2, Type3>

明确支持数组

如果我们明确知道是数组类型,可以用另一种方法。

// length is now recognised by declaring T as a type of array
function identity<T>(arg: T[]): T[] {
   console.log(arg.length);  
   return arg; 
}
//or
function identity<T>(arg: Array<T>): Array<T> {      
   console.log(arg.length);
   return arg; 
}

使用约束检查对象键是否存在

约束的一个很好的用例是通过使用另一段语法来验证一个键是否存在于对象上: extends keyof。下面的例子检查传递给函数的对象上是否存在键:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

第一个参数是我们要从中获取值的对象,第二个参数是该值的键。返回类型描述了这种与T[K]的关系,尽管这个函数在没有定义返回类型的情况下也可以工作。

这里泛型所做的是确保对象的键存在,这样就不会发生运行时错误。这是一个类型安全的解决方案,通过简单地调用let value = obj[key];

更多泛型的例子

API services

API services 是泛型的一个强大用例,允许您将API处理程序包装在一个类中,并在从不同端点获取结果时分配正确的类型。

class APIService extends API {
   public getRecord<T, U> (endpoint: string, params: T[]): U {}
   public getRecords<T, U> (endpoint: string, params: T[]): U[] {}
  ...
}

操作数组

class Department<T> {

   //different types of employees
   private employees:Array<T> = new Array<T>();

   public add(employee: T): void {
      this.employees.push(employee);
   }
   ...
}

继承类

class Programmer {
  // automatic constructor parameter assignment
  constructor(public fname: string,  public lname: string) { 
  }
}

function logProgrammer<T extends Programmer>(prog: T): void {
  console.log(`${ prog.fname} ${prog.lname}` );
}
const programmer = new Programmer("Ross", "Bulat");
logProgrammer(programmer); // > Ross Bulat

参考: