TypeScript 之 泛型 Generics 整理学习

441 阅读5分钟

一 泛型是什么

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

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值

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

/* 应用在函数里 */
function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}

console.log(identity<Number, string>(68, "Semlinker"));

编译器 自动选择类型

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

console.log(identity(68, "Semlinker"));

二 泛型接口

interface Person<T, N> {
    name: T;
    value: N
}
function identity<T, U> (value: T, message: U): Identities<T, U> {
  console.log(value + ": " + typeof (value));
  console.log(message + ": " + typeof (message));
  let identities: Identities<T, U> = {
    value,
    message
  };
  return identities;
}

console.log(identity(68, "Semlinker"));

三 泛型类

在类名后面使用, 定义任意多个类型变量

interface GenericInterface<U> {
  value: U
  getIdentity: () => U
}

class IdentityClass<T> implements GenericInterface<T> {
  value: T

  constructor(value: T) {
    this.value = value
  }

  getIdentity(): T {
    return this.value
  }

}

const myNumberClass = new IdentityClass<Number>(68);
console.log(myNumberClass.getIdentity()); // 68

const myStringClass = new IdentityClass<string>("Semlinker!");
console.log(myStringClass.getIdentity()); // Semlinker!

具体什么时候使用泛型类?

  1. 当你的函数、接口或类将处理多种数据类型时;
  2. 当函数、接口或类在多个地方使用该数据类型时。

四 泛型约束 extends

T extends Length 用于告诉编译器,我们支持已经实现 Length 接口的任何类型

之后,当我们使用不含有 length 属性的对象作为参数调用 identity 函数时, TypeScript 会提示相关的错误信息

我们还可以使用 , 号来分隔多种约束类型,比如:<T extends Length, Type2, Type3>

/* 1 */
interface Length {
  length: number;
}

function identity<T extends Length>(arg: T): T {
  console.log(arg.length); // 可以获取length属性
  return arg;
}
/* 2 设置为数组类型也可以解决上面的问题 */

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

检查对象上的键是否存在

keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型

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

结合 keyof 使用泛型

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

在以上的 getProperty 函数中,我们通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单调用 let value = obj[key]; 不同

五 泛型参数默认类型

为泛型中的类型参数,指定默认类型

interface A<T=string> {
  name: T;
}

const strA: A = { name: "Semlinker" };
const numB: A<number> = { name: 101 };

六 泛型条件类型 T extends U ? X : Y

条件类型,会以一个条件表达式进行类型关系检测

下面这个代码的意思是:

如果 T能够赋值给U,那么类型是X,否则是Y

在条件类型表达式中,我们通常还会结合 infer 关键字,实现类型抽取

T extends U ? X : Y

在上面示例中,当类型 T 满足 T extends Dictionary 约束时, 我们会使用 infer 关键字声明了一个类型变量 V,并返回该类型,否则返回 never 类型。

any 也不可以赋值给 never

interface Dictionary<T = any> {
  [key: string]: T;
}
 
type StrDict = Dictionary<string>

type DictMember<T> = T extends Dictionary<infer V> ? V : never
type StrDictMember = DictMember<StrDict> // string

利用条件类型和 infer 关键字,我们还可以方便地实现获取 Promise 对象的返回值类型

async function stringPromise() {
  return "Hello, Semlinker!";
}

interface Person {
  name: string;
  age: number;
}

async function personPromise() {
  return { name: "Semlinker", age: 30 } as Person;
}

type PromiseType<T> = (args: any[]) => Promise<T>;
type UnPromisify<T> = T extends PromiseType<infer U> ? U : never;

type extractStringPromise = UnPromisify<typeof stringPromise>; // string
type extractPersonPromise = UnPromisify<typeof personPromise>; // Person

七 泛型工具类型

为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record 和 ReturnType 等

/* Partial */
type Partial<T> = {
    [P in keyof T]?: T[P];
};
/* Record 将K中所有属性的值,设置为T */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};
interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const x: Record<Page, PageInfo> = {
  about: { title: "about" },
  contact: { title: "contact" },
  home: { title: "home" }
};
/* 
    Pick 
    Pick<T, K extends keyof T> 的作用是将某个类型中的子属性挑出来,
变成包含这个类型部分属性的子类型。
*/
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
}
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false
};
/* 
    Exclude 
    Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。

*/
// 如果 T 能赋值给 U 类型的话,那么就会返回 never 类型,否则返回 T 类型。
// 最终实现的效果就是将 T 中某些属于 U 的类型移除掉
type Exclude<T, U> = T extends U ? never : T;
// demo 
type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number
/* 
    ReturnType
    用于获取函数T的返回类型

*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer U ? U : any;
// demo

type T0 = ReturnType<() => string>; // string
type T1 = ReturnType<(s: string) => void>; // void
type T2 = ReturnType<<T>() => T>; // {}
type T3 = ReturnType<<T extends U, U extends number[]>() => T>; // number[]
type T4 = ReturnType<any>; // any
type T5 = ReturnType<never>; // any
type T6 = ReturnType<string>; // Error
type T7 = ReturnType<Function>; // Error

八 使用泛型创建对象

构造签名

interface Point {
  new (x: number, y: number): Point;
}

class Point2D implements Point {
  readonly x: number;
  readonly y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

function newPoint(
  pointConstructor: PointConstructor,
  x: number,
  y: number
): Point {
  return new pointConstructor(x, y);
}

const point: Point = newPoint(Point2D, 1, 2);

使用泛型创建对象

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

create<T>(c: { new(a: number): T; }, num: number): T {
  return new c(num);
}

const creator1 = new GenericCreator<FirstClass>();
const firstClass: FirstClass = creator1.create(FirstClass);

const creator2 = new GenericCreator<SecondClass>();
const secondClass: SecondClass = creator2.create(SecondClass);

参考资料

一文读懂 TypeScript 泛型及应用