TypeScript

TypeScript

  • 开始
  • 官方文档
  • 语言规范

›手册

文档说明

  • 文档约定
  • 历史版本

入门指南

  • 五分钟速览

手册

  • 数据类型
  • 变量声明
  • 接口
  • 类
  • 函数
  • 泛型
  • 枚举
  • 类型推断
  • 类型兼容
  • 高级类型

泛型

简介

软件工程师在开发组件时要定义明确且一致的 API,同时也要提高组件的复用性。能够处理现在和未来数据的组件将为你提供构建大型软件系统的灵活功能。

在像 C# 或 Java 的语言中,创建可重用组件的主要工具是泛型,使用泛型的组件可以处理很多的类型而不是单个类型。调用者可以使用这些组件并且让组件适配自己创建的类型。

泛型示例

我们创建一个泛型的例子开始学习,这个函数将返回输入的参数值,你可以把这个函数看作是 echo 命令。

没有泛型的话,我们需要给函数的参数及返回值设置明确的类型。

function identity(arg: number): number {
    return arg;
}

或者可以使用 any 类型。

function identity(arg: any): any {
    return arg;
}

但是使用 any 将导致函数接受任何的输入参数并失去类型信息。比如,我们传入一个 number,返回值是 any(~这种实现是不太好的)。

取而代之,我们需要一种捕获参数类型的方式,以便我们可以使用它来表示要返回的内容。这里,我们使用类型变量,这是一种特殊的适用于类型的变量。

function identity<T>(arg: T): T {
    return arg;
}

我们现在给函数加了一个类型变量 T,T 允许我们捕获调用者提供的类型信息(例如 number),所以之后函数中我们可以使用这个类型信息。这里我们设置函数的返回值类型为 T,现在参数和返回值都使用了相同的类型,这使我们可以将输入的类型信息原样输出。

现在我们就可以说 identity 函数是支持泛型的函数了,我们可以用两种方式来调用它。第一种方式是设置函数所有参数的值。

let output = identity<string>("myString");  // type of output will be 'string'

这里我们显式地设置函数的类型变量 T 为 string,用 <> 表示而不是 ()。

第二种方式更常见,我们使用类型参数推断,我们让编译器自动根据我们的输入参数设置类型变量 T 的值。

let output = identity("myString");  // type of output will be 'string'

注意我们不需要显式地使用 <> 来设置类型变量 T 的值,编译器仅仅查找 myString 的值,然后设置 T 为 myString 的类型。虽然类型参数推断能够让代码更简洁可读,但当编译器无法推断其类型时,你就需要使用第一种方式显式地设置类型变量的值。这种情况在复杂一点的例子中有可能会发生。

使用泛型类型变量

当你准备开始用泛型时,你需要注意在你创建像 identity 这样的泛型函数时,编译器将确保你可以在函数体中使用任何类型。也就是说,你可以将这些参数看成是任何类型。

function identity<T>(arg: T): T {
    return arg;
}

如果我们还希望每次调用时都将参数 arg 的长度记录到控制台,该怎么办?我们很有可能想这样做:

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

当我们这样写的时候,编译器会提示 arg.length 出错,因为我们并没有声明 arg 含有 length 属性。要记得,我们之前说过泛型类型变量代表所有的类型,所以完全可以给函数参数传递一个 number 类型的值,它当然没有 .length 属性。

假设我们实际上打算将此函数用于 T 的数组,而不是 T。我们知道数组是有 .length 属性的,我们可以像定义其他类型的数组一样使用泛型定义数组。

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

你可以认为:“泛型函数 loggingIdentity 拥有类型参数 T,参数 arg 是 T 类型的数组,函数也返回 T 类型的数组。” 如果我们向函数传递 number 类型的数组,我们也会得到 number 类型的数组,T 这个变量会被绑定为 number。这使我们可以将泛型变量 T 用作我们正在使用的类型的一部分,也提供了更大的灵活性。

也可以把代码修改成这样。

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

你可能已经很熟悉其他语言中的这种形式的代码。在下一部分中,我们将介绍如何创建类似 Array<T> 的自定义的泛型。

泛型

上一部分中,我们创建了适用于多种类型的泛型函数。在这部分,我们将探讨函数本身的类型以及如何创建泛型接口。

泛型函数的类型就像非泛型函数的类型一样,首先列出类型参数,类似于普通函数的声明。

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

泛型的类型变量可以使用不同的名字,只要类型变量的数量和使用方式和函数中的一致即可。

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <U>(arg: U) => U = identity;

我们还可以在对象字面量类型中使用泛型。

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: {<T>(arg: T): T} = identity;

上面的代码也可以用接口的方式来声明。

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

下面是一个类似的例子,我们想要把泛型变量移动到整个接口之上,这样我们可以看到整个接口的泛型(例如,Dictionary<string> 而不是 Dictionary)。通过这种方式,接口内部的其他成员都可以使用这个泛型变量。

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

注意,这个例子稍有不同了。现在,接口不再完整定义泛型函数,而仅是一个函数签名,该签名是泛型的一部分。现在当我们使用 GenericIdentityFn 时,就需要指定相应的类型参数(此处为:number)。这样就有效地锁定了泛型内成员所使用的类型。明白何时将类型参数放在函数调用方法签名上以及何时将其放在接口本身上对构建可重用组件是非常重要的。

除了泛型接口外,我们还可以创建泛型类。但是不能创建泛型枚举和命名空间。

泛型类

泛型类和泛型接口具有类似的结构,泛型类在类名后加上 <>,尖括号中间是类型参数的名字。

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

这是 GenericNumber 很简单的用法,但是你可能已经注意到程序中并没有说 T 只能是 number 类型,我们可以将类型参数 T 设置为 string 或者其他更复杂的类型。

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

像接口一样,将类型参数放在类本身之上将使得类的成员都可以使用该类型参数。

正如我们在类相关章节提到的一样。类是由两部分组成的:静态部分和实例部分。泛型只作用于类的实例部分而不包含静态部分。所以我们使用泛型时,静态成员不能使用泛型参数。

泛型的约束

如果你记得之前的例子,在对一组类型将具有的功能比较了解的前提下,有时你可能想写一个适用于一组类型的通用函数。在 loggingIdentity 例子中,我们想要访问参数 arg 的 .length 属性,但是编译器不能确认每种类型的参数都具有 .length 属性,所以编译器警告我们不能这样做。

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

与处理任何类型不一样的是,我们希望将此函数限制为具有 .length 属性的所有类型。只要类型有这个成员属性,我们就允许 T 可以为这个类型。为此,我们必须要对 T 能够适配的类型加一些限制条件。

我们创建一个接口来定义这个约束,这里,我们创建了只有一个 .length 属性的接口,然后我们可以使用接口和 extends 关键字来描述我们的约束。

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

在泛型约束中使用类型参数

你也可以定义一个被其他类型参数约束的类型参数。例如,这里我们想从给定名称的对象获取属性,我们想确保我们不会错误地获取对象不存在的属性,所以我们需要在两个类型之间建立约束。

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'.

在泛型中使用类

在 TypeScript 中使用泛型创建类似工厂模式的代码时,构造函数需要引用被创建的实例的类型。例如。

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

下面是一个更高级的示例,示例使用 prototype 属性来推断和约束构造函数和类型的实例之间的关系。

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!
← 函数枚举 →
  • 简介
  • 泛型示例
  • 使用泛型类型变量
  • 泛型
  • 泛型类
  • 泛型的约束
    • 在泛型约束中使用类型参数
    • 在泛型中使用类
Copyright © 2025 unimits