TypeScript学习笔记(五)泛型

176 阅读5分钟

泛型

用来创建可重用的组件,一个组件可以支持多种不同类型的数据

泛型之 Hello World

我们先来定义一个identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是echo命令。

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

使用any类型使这个函数可以接受任意类型的参数。 但是这样写有一个问题,就是不能表示接受的参数与返回的值类型是一致的,虽然我们知道这个函数返回的值就是它传入的值,它们的类型应该是一样的,但是我们还需要在函数中表示出来。 因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。就是类型变量,它是一个表示类型的变量,它不表示任何值。

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

这里我们定义了一个类型变量 T。它可以帮助我们捕获传入的参数类型,之后我们又将 T 设置为返回值的类型,这样我们就可以保证传入和返回值的类型是一致的了 —— 就是 T 表示的参数类型。 我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 定义好泛型后,我们有两种方法可以使用: 1、我们将类型变量的类型作为参数传入到函数中,用< >括起来,明确指定参数变量的类型

let output = identity<string>("myString");  

2、编译器会根据传入的参数自动地帮助我们确定T的类型,即类型推论

let output = identity("myString");  

使用泛型变量

参考上面的例子,如果我们同时想打印出arg的长度,就会报错。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // 报错: T 不存在 .length 方法
    return arg;
}

之所以会报这个错,是因为 T 代表的是任意类型,而使用这个方法的人有可能会传入一个number类型的参数,number是没有 .length属性的。

假设我们想操作的是 T 类型的数组,数组是有 .length 属性的,我们修改一下上面的例子

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // 数组有 `.length` 属性,不报错
    return arg;
}

也可以写成这样:

function loggingIdentity<T>(arg: <Array>T): <Array>T {
    console.log(arg.length);  // 数组有 `.length` 属性,不报错
    return arg;
}

你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时T的的类型为number

泛型类型

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样,我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以,比如上面例子中的 T 也可以写成 U,没有什么不同。

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

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

我们也可以通过接口来定义函数的参数类型:

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

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

let myIdentity: GenericIdentityFn = identity;

换个思路,我们也可以把泛型参数当成整个接口的一个参数,这样我们就能清楚的知道使用的具体是哪个泛型类型,接口内的其他成员也知道这个参数的类型了:

// 将泛型参数<T>作为接口的参数
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; };

我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。

泛型约束

在下面我们曾经看到的例子中,我们想获取arg的 .length 属性,但是报错了,因为arg有可能没有 .length 属性,

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

当我们想要去处理具有 .length 属性的所有类型,只要有这个属性,我们就允许,为此,我们需要列出对T的约束要求。 我们应该创建一个接口来描述约束条件:创建一个包含length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // 现在我们可以确定 arg 一定具有 .length 属性,所以不会再报错
    return arg;
}

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

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: m 不是变量 x 的 key 值。

分析一下这个例子: 函数getProperty 有两个类型参数,一个是T一个是包含泛型约束的 K,它的约束条件是 keyof T 就是必须是 T 中的某个 key 值, getProperty 的参数列表中的两个参数 objkey 分别需要满足 TK 这两个类型参数, getProperty(x, "m")不满足这个条件,因为 m 不是变量 x 的 key 值。