TypeScript中的泛型

307 阅读7分钟

泛型:Generics

  软件工程的主要部分就是构建一些即有声明良好且稳定的API又可重用的组件。而这些组件能帮助我们构建一个健壮且可扩展性强的系统。
  在像 C# 和 Java 这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
  设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。
  泛型(Generics)是允许同一个函数接受不同类型参数的一种模板。相比于使用 any 类型,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型。

泛型的语法

  对于刚接触 TypeScript 泛型的读者来说,首次看到 <T> 语法会感到陌生。其实它没有什么特别,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。 泛型的定义   参考上面的图片,当我们调用 identity<Number>(1)Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型。
  其中 T 代表 Type,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思:

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。   并不是只能定义一个类型变量,可以引入定义的任何数量的类型变量。比如我们引入一个新的类型变量 U,用于扩展我们定义的 identity 函数:
function identity<T, U>(value: T, message: U): T {
  console.log(message);
  return value;
}

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

定义多个类型   我们可以通过只传递参数来隐式调用函数,编译器会通过类型参数推理(type argument inference)的方式告知编译器我们使用了哪种类型作为类型T

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

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

  这种方式看起来更易读易写,但是在因对复杂的情况时,请不要省略类型变量,以防止造成不必要的错误。

泛型接口

interface GenericIdentityFn {
    <T>(arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;

  更进一步,我们可以通过将泛型类型变量提取到接口描述中,使接口中所有的函数、属性都接受泛型类型的描述,使其成为一个泛型接口。

interface GenericIdentityFn<T> {
    (arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

  在此时,我们使用接口来描述一个变量时,就要告知这个接口接收什么样的类型来描述泛型

泛型类

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };

  我们在讨论类的时候说到类其实有两个部分,一个是静态部分一个是实例部分。泛型只覆盖了实例部分。而在使用类的静态部分时,我们无法使用泛型来描述它的静态部分。

泛型约束

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}
loggingIdentity(1)

  在这个🌰中,我们需要访问arg的length属性,而编译器不能确保所有的类型全都有这个属性,所以它会报错。而通常我们在使用这个函数时,我们期望的并不是所有的类型,而是带有length属性的类型。所以如果一个变量有一些成员,且某些成员是必须的,我们就需要列出需求清单来约束传入的类型。
  为了解决这个问题,我们通过声明一个接口来描述我们的约束。在这里,我们创建一个带有length属性的接口,并使类型T扩展它来作为约束。

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}
loggingIdentity(3);  // Error, number doesn't have a .length property
loggingIdentity({length: 10, value: 3});

  这样就意味着在做类型检查时,我们会对参数进行检查,看看这个类型是否符合接口的描述——即传入的对象必须有一个实例部分的属性,名字叫做length且类型是number。如果调用的参数不符合这个约束,则编译器会报错。
  同样我们可以使用泛型来扩展泛型。

function copyFields<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = source[id];
    }
    return target;
}

let x = { a: 1, b: 2, c: 3, d: 4 };
copyFields(x, { b: 10, d: 20 }); // okay
copyFields(x, { Q: 90 });  // error: property 'Q' isn't declared in 'x'.

  这个描述表示的是,我们接受两个泛型类型的参数用作函数的参数,而第一个类型要被第二个类型所约束,即第二个类型的对象属性必须存在于第一个类型的对象属性列表中。否则就会报错。我们可以将第一个类型看成是子类而第二个类型看成是父类,但是要求并不如继承那样严格罢了。

泛型工具类

  为了方便开发者 TypeScript 内置了一些常用的工具类型,比如 Partial、Required、Readonly、Record、ReturnType 等。这里我们只简单介绍 Partial 工具类型。不过在具体介绍之前,我们得先介绍一些相关的基础知识。

  • keyof   keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
interface Person {
  name: string;
  age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number

  在 TypeScript 中支持两种索引签名,数字索引和字符串索引:

interface StringArray {
  // 字符串索引 -> keyof StringArray => string | number
  [index: string]: string; 
}

interface StringArray1 {
  // 数字索引 -> keyof StringArray1 => number
  [index: number]: string;
}

  为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。所以 keyof { [x: string]: Person } 的结果会返回 string | number

  • in   in 用来遍历枚举类型:
type Keys = "a" | "b" | "c"

type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }
  • Partial   Partial<T> 的作用就是将某个类型里的属性全部变为可选项 ?。定义如下:
/**
 * node_modules/typescript/lib/lib.es5.d.ts
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

  在以上代码中,首先通过 keyof T 拿到 T 的所有属性名,然后使用 in 进行遍历,将值赋给 P,最后通过 T[P] 取得相应的属性值。中间的 ? 号,用于将所有属性变为可选。

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>) {
  return { ...todo, ...fieldsToUpdate };
}

const todo1 = {
  title: "Learn TS",
  description: "Learn TypeScript",
};

const todo2 = updateTodo(todo1, {description: "Learn TypeScript Enum"});

  在上面的 updateTodo 方法中,我们利用 Partial<T> 工具类型,定义 fieldsToUpdate 的类型为 Partial<Todo>,即:

{ 
    title?: string | undefined; 
    description?: string | undefined;
}

泛型参数的默认类型

  在 TypeScript 2.3 以后,我们可以为泛型中的类型参数指定默认类型。当使用泛型时没有在代码中直接指定类型参数,从实际值参数中也无法推测出时,这个默认类型就会起作用。

function createArray<T = string>(length: number, value: T): Array<T> {
    let result: T[] = [];
    for (let i = 0; i < length; i++) {
        result[i] = value;
    }
    return result;
}