Skip to content

泛型

本文参考自

一、泛型的作用

在 TypeScript 中我们会经常遇到一些接收类型或者输出类型不确定的情况。例如:

ts
function identity (str: string | number): string | number {
    return str
}
identity ('aa')
identity (123)

identity 函数能根据输入 str 的值返回相同的值,这里 str 的类型只限制在了 string 和 number 两种类型。但是如果 str 是在不止两种类型甚至未知类型情况下,该如何编写 identity 函数呢?

这是就需要我们的泛型出场来解决这个问题。

ts
function identity <T>(str: T): T {
    return str
}

可以看到我们在函数 identity 后面引入了泛型 <T>,然后将泛型当作类型使用在 str 和 identity 函数返回类型上。

此时 T 只代表一个占位符,具体是什么数据类型我们不知道。只有在调用函数时,根据传入的类型来替换 T 。

ts
identity <string>('aa')
identity <number>(123)

例如: 第一个 identity 传入了 string,那么到函数 identity 里面的 T 就是代表 string 类型。

可以看到泛型的主要作用就是帮助我们接收未知的数据类型。从而达到扩展功能性。

二、泛型接口

泛型接口就是泛型与接口结合的使用。例如 在接收多个泛型,而函数需要返回多个泛型值情况下:

先定义函数返回值接口

ts
interface Identities<V, M> {
    value: V,
    message: M
}

然后在函数返回值上使用接口 Identities

ts
function identity<T, U> (value: T, message: U): Identities<T, U> {
    let identities: Identities<T, U> = {
        value,
        message
    };
    return identities;
}

三、泛型类

在类中使用泛型也很简单,我们只需要在类名后面,使用 <T, ...> 的语法定义任意多个类型变量,具体示例如下:

ts
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!

四、泛型约束

4.1 确保属性存在

有时候我们函数内会调用参数的某个属性或方法,但这个参数类型是泛型时,我们应该如何确保这个参数拥有这个属性或方法呢?

ts
function identity<T>(arg: T): T {
    console.log(arg.length)
    return arg
}

例如当 arg 如果不是数组或字符串时,就不回具有 length 属性。

这时我们需要做的就是让类型变量 extends 一个含有我们所需属性的接口,比如这样:

ts
interface Length {
    length: number;
}

function identity<T extends Length>(arg: T): T {
    console.log(arg.length); // 可以获取length属性
    return arg
}

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

4.2 查对象上的键是否存在

泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。

其中需要借助 keyof 操作符,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。

ts
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 操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。具体的使用方式如下:

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

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

下面我们来看一下如何使用 getProperty 函数:

ts
enum Difficulty {
    Easy,
    Intermediate,
    Hard
}

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

let tsInfo = {
   name: "Typescript",
   supersetOf: "Javascript",
   difficulty: Difficulty.Intermediate
}
 
let difficulty: Difficulty = getProperty(tsInfo, 'difficulty'); // OK

let supersetOf: string = getProperty(tsInfo, 'superset_of'); // Error

五、泛型参数默认类型

泛型参数默认类型与普通函数默认值类似,对应的语法很简单,即 <T=Default Type>,对应的使用示例如下:

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

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

泛型参数的默认类型遵循以下规则:

  • 有默认类型的类型参数被认为是可选的。
  • 必选的类型参数不能在可选的类型参数后。
  • 如果类型参数有约束,类型参数的默认类型必须满足这个约束。
  • 当指定类型实参时,你只需要指定必选类型参数的类型实参。 未指定的类型参数会被解析为它们的默认类型。
  • 如果指定了默认类型,且类型推断无法选择一个候选类型,那么将使用默认类型作为推断结果。
  • 一个被现有类或接口合并的类或者接口的声明可以为现有类型参数引入默认类型。
  • 一个被现有类或接口合并的类或者接口的声明可以引入新的类型参数,只要它指定了默认类型

六、泛型条件类型

条件类型会以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:

ts
T extends U ? X : Y

以上表达式的意思是:若 T 能够赋值给 U,那么类型是 X,否则为 Y。在条件类型表达式中,我们通常还会结合 infer 关键字,实现类型抽取:

ts
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

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