TypeScript 类型编程进阶:从泛型到类型体操

作者:半夏

时间:2026-10-25

浏览:611

本文深入解析 TypeScript 类型系统的演进,从基本类型到泛型,再到高阶的类型编程(类型体操)。详细讲解 Pick、Exclude 和 ReturnType 等内置高级类型的实现原理,重点剖析 extends、infer 及条件类型的用法,帮助开发者掌握 TS 类型推导的核心技巧。

文章目录

TypeScript 类型编程

TypeScript 的类型体系基础源自 JavaScript 的基本类型,如 string、number 和 boolean。在此基础上,TS 扩展了 tuple、enum、复合类型、交叉类型以及索引类型等增强特性,构建了更丰富的类型表达。

在实际开发中,若函数需要处理不同类型的数组并提取元素,传统方式需为每种类型单独编写类型别名,导致代码冗余且维护困难。

这种针对每种数组类型重复定义 type 别名的做法显得过于繁琐,缺乏通用性。

type getStrItem = (items: string[]) => string;
type getNumItem = (items: number[]) => number;
// ... 每增加一种类型都要写多了一个 type 别名
const getStrFirst: getStrItem = (a) => {
    return a[0];
}

为此,TypeScript 引入了泛型机制,允许将类型作为参数传递,从而提升代码的复用性。

type getItem<T> = (items: T[]) => T
const getStrFirst: getItem<string> = (a) => {
    return a[0];
}

上述示例中的 T 即为类型参数,通过 类型别名<具体类型> 的形式(对应代码中的 getItem<string>),我们可以实例化出具体类型的泛型定义。

由于 JavaScript 具有高度灵活性,TypeScript 采用结构类型系统。即便泛型提供了基础推导,有时仍需进一步细化 T 的内部结构,以获取更精确的类型信息。

基于泛型,TypeScript 提供了类型编程能力,利用特定语法提取 T 中更细粒度的类型,或通过条件判断生成其他类型。

这种技术常被开发者戏称为“类型体操”,因其实现方式复杂多变,如同参与高难度的体操比赛。

从类型能力的演进角度来看,大致经历了以下过程:

基本类型 -> 泛型 -> 类型编程(类型体操)

TypeScript 内置高级类型

本文示例基于 TS 4.8.2 版本。

接下来,我们将探讨几个利用类型编程实现的内置高级类型。

Pick<Type, Keys>

Pick 类型的作用是从对象类型 T 中提取由联合类型 K 指定的属性键,并返回一个新的对象类型。

TypeScript 类型编程

通过 Pick,我们可以精准提取出 pos 和 radius 等所需的物理信息属性。

以下是 Pick 的具体实现代码:

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

首先观察等号左侧的 <T, K extends keyof T>,其中包含两个类型参数:T 和 K。

关于类型参数的命名规范。

类型变量的命名规则与 JavaScript 变量类似,可自由定义。但建议首字母大写,以避免与 extends、as、infer 等小写关键字混淆。

通常 T 代表待分析的类型(Type),K 代表对象属性名(Key)。若难以确定命名,沿用这两个字母即可,类似于数学中的 x 和 y。

keyof 是类型运算符,用于提取对象的属性键并组合成联合类型。

TypeScript 类型编程

extends 用于约束类型参数的范围。例如 <T extends string> 表示 T 必须是 string 的子类型,包括字面量 "a" 或 string 本身。若不符合子类型约束,编译将报错。

此外,还存在类似 JS 三元运算符的 extends ? : 语法,位于等号右侧,用于条件判断。这与前述的类型约束 extends 并非同一概念,后文将详述。

整体来看,<T, K extends keyof T> 表示传入 T 和 K 两个类型参数,且 K 必须是 T 属性联合类型的一部分。

再看等号右侧的 { [P in K]: T[P]; };,这涉及类型的重映射操作。

in 关键字用于遍历联合类型,将所需的 key 作为索引 P,其值对应 T[P]。

Exclude<UnionType, ExcludedMembers>

Exclude 的作用是从联合类型中剔除指定的类型。

其实现方式如下:

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

此处涉及一个常用条件语法:extends ? :,可类比 JS 中的三元表达式(即 condition ? a : b)。

为便于理解,我们实现一个 IsNumber 类型,用于判断给定类型是否为 number。

type IsNumber<T> = T extends number ? true : false;
// 使用
type A = IsNumber<1> // true
type B = IsNumber<"str"> // false

T extends number 用于判断 T 是否为 number 的子类型,若是则返回 true,否则返回 false。

需注意,此处的 extends 与前述类型参数约束中的 extends 含义完全不同。

回到 Exclude,其逻辑清晰:判断 T 是否为 U 的子类型,若是则返回 never(即被丢弃),否则返回 T。

TypeScript 类型编程

读者可能会疑惑,逻辑上 "a" | "b" | "c" 并非 "b" 的子类型,为何结果不是原样返回,而是变成了 "a" | "c"?

这源于联合类型的特殊处理机制:当联合类型使用 extends 时,它会被打散为独立的类型分别进行判断,最后再重新组合。

具体而言,"a" | "b" | "c" 被拆解,依次判断 "a"、"b"、"c" 是否为 "b" 的子类型,结果分别为 "a"、never、"c",最终联合得到 "a" | "c"。

ReturnType<Type>

用于获取函数类型的返回值类型。

TypeScript 类型编程

具体实现如下:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

等号左侧的 (...args: any) => any 表示任意函数类型,用于限制传入参数的类型。

此处引入了新关键词 infer,表示引用,用于类型推导。

extends 与 infer 配合可实现模式匹配:若 extends 匹配成功,infer 即可推导出对应类型。

熟悉 JS 正则表达式的读者会发现,这类似于正则中的捕获组,infer 起到了类似作用。

'ABC'.replace(/A(.)C/, '$1') 
// 'B'。提取了模式上匹配的一个字符串

在 T extends (...args: any) => infer R ? R : any; 中,我们对返回值部分设置 infer,并提供局部变量 R。

若 extends 条件判断为继承关系,则变量 R 被赋值为函数的返回值。

后续判断为真的分支(? 后的表达式)即可获取该 R;若判断为假,则因匹配失败无法获取。

这种 extends 结合 infer 的模式正是类型体操的核心,允许在传入类型 T 的基础上继续拆分,获取更细粒度的类型。

更多类型体操学习

受篇幅限制,此处不再赘述其他类型编程技巧,例如:

  • as 运算符可用于类型索引的重映射;
  • 利用数组的 "length" 属性可实现数字运算;
  • 通过递归实现循环逻辑;
  • 处理 never 等特殊类型等。

TypeScript 的类型系统是图灵完备的,能够实现各种判断、循环及加减逻辑,尽管部分逻辑实现较为繁琐。

其语法独特,进行了“压缩”处理:一个类型的编程仅是一个表达式,需通过 extend ? : 不断嵌套来实现逻辑。学习 TS 类型体操在某种程度上如同学习一门新语言,且带有一定古怪性。

以上为 TypeScript 内置高级类型编程示例的详细内容,更多关于 TypeScript 内置类型的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章