让我害怕的 TypeScript 类型 — — 直到我学会了这 3 条规则

56 阅读5分钟

原文:The TypeScript Types That Terrified Me — Until I Learned These 3 Rules

作者:Steve Sewell

我曾经对 TypeScript 的某些高级类型充满了敬畏(甚至是恐惧)。infer、条件类型(Conditional Types)、映射类型(Mapped Types)……这些东西看起来就像是黑魔法,强大,但又深不可测。

我尝试过去阅读官方文档,但那些密密麻麻的理论和抽象的例子,常常让我看得云里雾里,感觉自己智商受到了挑战。我能看懂每个单词,但连在一起就不知道是啥意思了。

直到有一天,我顿悟了。我发现,理解这些“恐怖”类型的关键,并不在于死记硬背那些复杂的语法,而在于掌握它们背后的一些核心规则和模式。就像学武功要先懂心法一样。

今天,我就把我的“心法”——这 3 条黄金规则——分享给你,希望能帮你揭开这些高级类型的神秘面纱,让你也能轻松驾驭它们。

规则一:把类型当成“变量”,用条件来“编程”

我们先从条件类型说起。它的语法是这样的:

SomeType extends OtherType ? TrueType : FalseType;

是不是看起来很像 JavaScript 里的三元运算符?没错,它的思想是完全一样的!

  • SomeType 就是你要检查的“变量”。
  • extends 就是你的“判断条件”。
  • ? TrueType : FalseType 就是你的“if/else”分支。

核心思想: 条件类型让你的类型定义“动”了起来,你可以根据一个类型是否满足某个条件,来返回不同的类型。

举个栗子,我们想写一个类型,如果传入的是 string,就返回 true,否则返回 false

type IsString<T> = T extends string ? true : false;

let a: IsString<'hello'>; // a 的类型是 true
let b: IsString<123>;    // b 的类型是 false

看,是不是很简单?你就在用类型来“编程”!

规则二:infer 不是推断,而是“声明一个新变量”

infer 关键字绝对是很多人的噩梦。官方文档说它是“推断”,但这个词太抽象了。我更喜欢把它理解为**:在 extends 条件语句中,声明一个临时的、局部的类型“变量”**。

核心思想infer 就像一个占位符,它会捕获在类型匹配过程中,那个位置上实际的类型,然后你就可以在 true 分支里使用这个被捕获的类型了。

让我们来看一个经典的例子:获取一个函数类型的返回类型。

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

我们来拆解一下这个“咒语”:

  1. T extends (...args: any[]) => infer R:这是我们的判断条件。我们在问:“T 是不是一个函数类型?”
  2. infer R:这就是魔法发生的地方!我们在这里“声明”了一个新的类型变量 R。如果 T 真的是一个函数,那么 R 就会被赋值为这个函数的返回类型。
  3. ? R : never:如果 T 是函数,我们就返回我们刚刚捕获到的返回类型 R。如果不是,就返回 never(一个表示“永不存在”的类型)。
type MyFunc = () => string;
type MyString = GetReturnType<MyFunc>; // MyString 的类型是 string

type NotAFunc = number;
type NeverHappens = GetReturnType<NotAFunc>; // NeverHappens 的类型是 never

所以,别再把 infer 想成什么高深的“推断”了,就把它当成你在 if 语句里声明的一个局部变量,是不是一下子就清晰了?

规则三:映射类型就是类型的“for...in”循环

映射类型(Mapped Types)允许你基于一个已有的类型,来创建出一个新的类型。它的语法可能会让你有点晕:

type MappedType<T> = {
  [P in keyof T]: SomeNewType;
};

别怕,我们还是用 JavaScript 的思想来理解它。这玩意儿,本质上就是一个针对类型的 for...in 循环。

  • keyof T:这会获取类型 T 所有的键(keys),组成一个联合类型。就像 Object.keys()
  • [P in keyof T]:这就是 for (const P in Object.keys(T))。它会遍历 T 的每一个键。
  • : SomeNewType:在循环体内部,你可以对每个属性的类型进行转换,定义新的类型。

核心思想: 映射类型让你能够批量地、动态地修改一个对象类型中所有属性的类型。

举个栗子,我们想把一个类型的所有属性都变成只读的。TypeScript 内置的 Readonly<T> 就是用映射类型实现的:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

interface User {
  name: string;
  age: number;
}

type ReadonlyUser = Readonly<User>;
// ReadonlyUser 的类型是:
// {
//   readonly name: string;
//   readonly age: number;
// }

我们来分析一下:

  1. [P in keyof User]:遍历 User 的键,也就是 'name''age'
  2. readonly:给每个属性加上 readonly 修饰符。
  3. : T[P]:属性的类型保持不变(T[P] 就是获取原类型 TP 属性的类型)。

通过这个“循环”,我们轻松地创建了一个所有属性都变成只读的新类型。

总结:用编程思维去理解类型

现在,我们再回过头来看这些“恐怖”的类型:

  • 条件类型:就是类型的 if/else
  • infer:就是在 if 语句里声明一个局部变量。
  • 映射类型:就是类型的 for...in 循环。

你看,一旦我们把它们和熟悉的编程概念联系起来,它们就不再那么可怕了。它们只是工具,是 TypeScript 赋予我们用来操作和转换类型的强大武器。

下次当你再遇到这些高级类型时,不要慌。深呼吸,然后用这三条规则去“翻译”它们。你会发现,你也能成为驾驭 TypeScript 类型的“魔法师”。