TypeScript泛型学习与实践 | 青训营

833 阅读7分钟

TypeScript泛型概述

借用Java中泛型的释义可知泛型指的是类型参数化,通俗理解就是将原先的某种具体类型进行参数化。同理定义函数的参数,我们可以给泛型定义若干类型参数,并在调用时给泛型传入明确的类型参数。
更重要的是,设计泛型可有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。

(通过学习实践进行体会,如下)
首先定义一个通用 identity 函数,函数接收一个参数并直接返回该参数

function identity (value) {
  return value;
}
console.log(identity(1))

对函数进行设置使其支持 Number 类型的参数

function identity (value: Number) : Number {
  return value;
}
console.log(identity(1))

出现问题:我们将 Number 类型分配给参数和返回类型,使该函数只可用于该原始类型,该函数并不是可拓展的,这不是我们追求的。如果考虑将类型设为 any ,就会失去定义返回类型的能力,而且编译器还失去了类型保护的作用。目标应该是 identity 函数可以适用于任何特定的类型,为了实现此效果可以使用泛型,如下

function identity <T>(value: T) : T {
  return value;
}
console.log(identity<Number>(1))

image.png

就像传递参数一样,我们传递了我们想要用于特定函数调用的类型 image.png 参考上图,当我们调用 identity<Number>(1) ,Number 类型就像参数 1 一样,它将在出现 T 的任何位置填充该类型。图中 <T> 内部的 T 被称为类型变量,它是我们希望传递给 identity 函数的类型占位符,同时它被分配给 value 参数用来代替它的类型:此时 T 充当的是类型,而不是特定的 Number 类型
T 代表 Type,在定义泛型时通常用作第一个类型变量名称,它也可以用任何有效名称代替。另外还有如 K(Key)表示对象中的键类型、V(Value)表示对象中的值类型、E(Element)表示元素类型。

继续引入一个新的类型变量 U ,拓展函数

function identity <T, U>(value: T, message: U) : T {
  console.log(message);
  return value;
}
console.log(identity<Number, string>(123, "Hello"));

image.png (而且除了为类型变量显示设定值外,更常用的方法还有使编译器自动选择类型,即可以省略掉<Number, string>,编译器能够知道我们的参数类型,并将它们赋值给 T 和 U )

接着思考一个问题:新的函数增加了一个类型变量 U ,但该函数的返回类型我们仍然使用 T ,若想要返回两种类型的对象如何做?一种方式是采用元组,为元组设置通用的类型

function identity <T, U>(value: T, message: U) : [T, U] {
  return [value, message];
}

但还有更合适的方式,即考虑采用泛型接口

泛型接口

针对前面想到的问题,考虑采用泛型接口,首先创建一个用于 identity 函数的通用 Identities 接口,之后就可以将该接口作为 identity 函数的返回类型

interface Identities<V, M> {
  value: V,
  message: M
}
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(123, "Hello"));

(运行结果) image.png

泛型类

想在类中使用泛型只需在类名后面使用 <T, ...> 的语法定义任意多个类型变量(实践示例如下)

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>(123);
console.log(myNumberClass.getIdentity());
const myStringClass = new IdentityClass<string>("Hello");
console.log(myStringClass.getIdentity());

image.png

在实例化 IdentityClass 对象时,我们传入 Number 类型和构造函数参数值 123 ,之后在 IdentityClass 类中,类型变量 T 的值变成 Number 类型。IdentityClass 类实现了 GenericInterface<T>, T 表示 Number 类型,因此等价于该类实现了 GenericInterface<Number> 接口。而对于 GenericInterface<U> 接口来说,类型变量 U 也变成了 Number 。

(积累学习)
泛型类可确保在整个类中一致地使用指定的数据类型。例如在使用 Typescript 的 React 项目中使用了以下约定

image.png

上面代码中,将泛型与 React 组件一起使用,以确保组件的 props 和 state 是类型安全的。
这时就会思考一个问题:何时需要使用泛型?参考标准有什么?
1、当函数、接口或类将处理多种数据类型时;2、当函数、接口或类在多个地方使用该数据类型时;
由于没有办法保证在项目早期就使用泛型的组件,但随着项目的开发,组件的功能通常会被扩展。这种增加的可扩展性最终很可能会满足上述两个条件,在这种情况下,引入泛型将比复制组件来满足一系列数据类型更清晰干净

类型约束

关键问题:如何使用类型约束来增加代码的灵活性和安全性?
泛型不仅可以提供灵活性,还可以增加代码的安全性,通过使用类型约束可以限制泛型参数的类型范围
(1)确保属性的存在
有时我们希望类型变量对应的类型上存在某些属性。但除非我们显式地将特定属性定义为类型变量,否则编译器不会知道它们的存在。举个例子:在处理字符串或数组时,我们假设 length 属性是可用的,再次使用 identity 函数并尝试输出参数的长度

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

image.png

这种情况下,编译器不会知道 T 确实含有 length 属性,特别是在可以将任何类型赋给类型变量 T 的情况下。我们应该要让类型变量 extends 含有我们所需属性的接口

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

image.png

T extends Length 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。以后如果使用不含有 length 属性的对象作为参数调用 identity 函数时,TypeScript 就会提示相关的错误信息。
另外还可以使用逗号来分隔多种约束类型,比如:<T extends Length, Type2, Type3>。对于上述 length 属性问题来说,如果显式地将变量设置为数组类型也可以达到目的。
image.png

(2)检查对象上的键是否存在
在实践操作前,先了解学习了 keyof 操作符, keyof 操作符是在 TypeScript 2.1 版本引入的,该操作符可以用于获取某种类型的所有键,其返回类型是联合类型。
image.png

通过 keyof 操作符,就可以获取指定类型的所有键,接着就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。

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 函数) image.png 通过实践可知,使用类型约束,我们在编译阶段就可以提前发现错误,提高了程序的稳定性和安全性。

总结

泛型是 TypeScript 的一强大特性,我们可以在函数、接口和类中使用泛型,使用类型约束来限制泛型参数的类型范围,增加代码的灵活性和安全性。在自己学习和实践 TypeScript 泛型的过程中,也发现有关泛型的知识点还有很多,需要继续积累和完善知识点,并通过代码的实践来加深理解。