前端深入理解 TypeScript 泛型概念

作者:代码拾荒

时间:2026-10-23

浏览:511

本文深入解析 TypeScript 中的泛型机制,涵盖泛型函数、接口、类及约束的应用。通过具体代码示例,演示如何利用类型参数保持类型安全,解决代码冗余问题,并掌握 keyof 索引类型与类类型约束的高级用法,帮助开发者编写更健壮的前端代码。

文章目录

泛型基本概念

首先介绍泛型的基本概念。

泛型程序设计(generic programming)是程序设计语言的一种风格或范式。它允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。

泛型是指在定义函数、接口或者类的时候,不预先定义好具体的类型,而在使用的时候再指定类型的一种特性。

先举一个简单的例子。

假设我们定义一个函数,它可以接收一个 number 类型作为参数,并且返回一个 number 类型。


function genericDemo(data: number): number {
  return data;
}

按照以上的写法是没有问题的,但是如果我们要接受一个 string 并返回一个 string 呢?如果逻辑一样还要再写一遍吗?就像下面这样。


function genericDemo(data: string): string {  
  return data;
}

这显然代码是很冗余的,我们还有不使用 any 的写法吗?答案是显然易见的,可以使用泛型的写法,就像下面这样。


function genericDemo<T>(data: T):T {
  return data;
}

泛型函数与类型参数

我们在函数名称 genericDemo 后面声明了泛型变量 <T>,它用于捕获调用该函数时传入的参数类型(例如:number),之后我们就可以使用这个类型。之后我们再次使用了 T 当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。这允许我们跟踪函数里使用的类型的信息。

多个类型参数。

我们在定义泛型的时候,也可以一次定义多个类型参数,像下面这样。


function swap<T, U>(tuple: [T, U]):[U, T] {
  return [tuple[1], tuple[0]];
}

泛型接口应用

泛型接口。

我们先定义一个泛型接口 Identities,然后定义一个函数 identities() 来使用这个泛型接口。


interface Identities<T, U> {
  id1: T;
  id2: U;
}

我在这里使用 T 和 U 作为我们的类型变量来演示任何字母(或有效的字母数字名称的组合)都是有效的类型——除了常规用途之外,您对它们的调用没有任何意义。

我们现在可以将这个接口应用为 identity() 的返回类型,修改我们的返回类型以符合它。我们还可以 console.log 这些参数和它们的类型,以便进一步说明:


function identities<T, U> (arg1: T, arg2: U): Identities<T, U> {
  console.log(arg1 + ": " + typeof (arg1));
  console.log(arg2 + ": " + typeof (arg2));
  let identities: Identities<T, U> = {
  id1: arg1,
  id2: arg2
 };
 return identities;
}

我们现在对 identity() 所做的是将类型 T 和 U 传递到函数和 identity 接口中,从而允许我们定义与参数类型相关的返回类型。

泛型变量与类型安全

泛型变量。

使用泛型创建像 identity 这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。换句话说,你必须把这些参数当做是任意或所有类型。

我们先看下之前例子。


function genericDemo<T>(data: T):T {
  return data;
}

如果我们想同时打印出 data 的长度。我们很可能会这样做。


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

如果这么做,编译器会报错说我们使用了 data 的 .length 属性,但是没有地方指明 data 具有这个属性。记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length 属性的。

现在假设我们想操作 T 类型的数组而不直接是 T。由于我们操作的是数组,所以 .length 属性是应该存在的。我们可以像创建其它数组一样创建这个数组:


function genericDemo<T>(data: Array<T>):Array<T> {
  console.log(data.length);
  return data;
}

泛型类与 React 实践

泛型类。

我们还可以在类属性和方法的意义上使类泛型。泛型类确保在整个类中一致地使用指定的数据类型。例如下面这种在 React TypeScript 项目中的写法。


interface Props {
  className?: string;
  ...
}

interface State {
  submitted?: bool;
  ...
}

class MyComponent extends React.Component<Props, State> {
  ...
}

我们在这里使用与 React 组件一起使用的泛型,以确保组件的 props 和 state 是类型安全的。

泛型约束与索引类型

泛型约束。

我们先看一个常见的需求,我们要设计一个函数,这个函数接受两个参数,一个参数为对象,另一个参数为对象上的属性,我们通过这两个参数返回这个属性的值,比如:


function getValue(obj: object, key: string){
  return obj[key] // error
}

我们会得到一段报错,这是新手 TypeScript 开发者常常犯的错误,编译器告诉我们,参数 obj 实际上是 {},因此后面的 key 是无法在上面取到任何值的。

因为我们给参数 obj 定义的类型就是 object,在默认情况下它只能是 {},但是我们接受的对象是各种各样的,我们需要一个泛型来表示传入的对象类型,比如 T extends object:


function getValue<T extends object>(obj: T, key: string) {
 return obj[key] // error
}

这依然解决不了问题,因为我们第二个参数 key 是不是存在于 obj 上是无法确定的,因此我们需要对这个 key 也进行约束,我们把它约束为只存在于 obj 属性的类型,这个时候需要借助到后面我们会进行学习的索引类型进行实现 <U extends keyof T>,我们用索引类型 keyof T 把传入的对象的属性类型取出生成一个联合类型,这里的泛型 U 被约束在这个联合类型中,这样一来函数就被完整定义了:


function getValue<T extends object, U extends keyof T>(obj: T, key: U) {
 return obj[key] // ok
}

另外提一个多重泛型约束的写法,可以当作拓展:


interface firstInterface {
  first(): number
}

interface secondInterface {
  second(): string
}

class Demo<T extends firstInterface & secondInterface >{
  ...
}

泛型中的类类型与工厂函数

在泛型里使用类类型。

在 TypeScript 使用泛型创建工厂函数时,需要引用构造函数的类类型。比如:


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

参数 type 的类型 {new(): T} 就表示此泛型 T 是可被构造的,在被实例化后的类型是泛型 T。

总结。

到此这篇关于前端深入理解 TypeScript 泛型概念的文章就介绍到这了,更多相关 TypeScript 泛型内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章