条件类型(Conditional Types)

在大多数有用的程序中,我们都需要根据输入做出决策。JavaScript 程序也不例外,鉴于值可以轻松地被检查,这些决策也会基于输入的类型。条件类型用于描述输入类型与输出类型之间的关系。

ts 复制代码
interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}

type Example1 = Dog extends Animal ? number : string;
// Example1 = number

type Example2 = RegExp extends Animal ? number : string;
// Example2 = string

条件类型的语法类似于 JavaScript 中的条件表达式 (条件 ? 真值 : 假值):

ts 复制代码
SomeType extends OtherType ? TrueType : FalseType;

当左边的类型可以赋值给右边的类型时,返回前者("真"分支);否则返回后者("假"分支)。

虽然上面的例子看起来用处不大(我们知道 Dog 是否继承自 Animal),但条件类型的强大之处在于结合泛型使用。

比如,下面是一个 createLabel 函数的定义:

ts 复制代码
interface IdLabel {
  id: number;
}
interface NameLabel {
  name: string;
}

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

这个函数的多个重载会随着类型数量的增长而变得难以维护。我们可以使用条件类型来简化它:

ts 复制代码
type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}

使用示例:

ts 复制代码
let a = createLabel("typescript"); // NameLabel
let b = createLabel(2.8);          // IdLabel
let c = createLabel(Math.random() ? "hello" : 42); // NameLabel | IdLabel

条件类型中的约束(Constraints)

有时,条件类型中的判断会为我们提供新的信息。就像类型守卫会缩小类型范围一样,条件类型的真分支会进一步限制泛型的类型。

ts 复制代码
type MessageOf<T> = T["message"]; // 错误:T 不一定有 message 属性

// 解决方法:增加约束
type MessageOf<T extends { message: unknown }> = T["message"];

interface Email {
  message: string;
}

type EmailMessageContents = MessageOf<Email>; // string

若我们想让 MessageOf 支持任意类型,并在没有 message 属性时返回 never:

ts 复制代码
type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;

interface Dog {
  bark(): void;
}

type DogMessageContents = MessageOf<Dog>; // never

在条件类型的真分支中,TypeScript 会知道 T 拥有 message 属性。

另一个例子是写一个 Flatten 类型,将数组类型扁平化为其元素类型,否则返回原类型:

ts 复制代码
type Flatten<T> = T extends any[] ? T[number] : T;

type Str = Flatten<string[]>; // string
type Num = Flatten<number>;   // number

条件类型中的推断(infer)

我们可以使用 infer 关键字在条件类型的真分支中引入新的泛型类型变量:

ts 复制代码
type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

更复杂的用法是提取函数的返回值类型:

ts 复制代码
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

type Num = GetReturnType<() => number>; // number
type Str = GetReturnType<(x: string) => string>; // string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]

对于多个重载签名的函数(如下所示),推断会基于最后一个签名进行:

ts 复制代码
declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;

type T1 = ReturnType<typeof stringOrNum>; // string | number

分布式条件类型(Distributive Conditional Types)

当条件类型作用于泛型时,它们会在联合类型上"分布式"应用:

ts 复制代码
type ToArray<Type> = Type extends any ? Type[] : never;

type StrArrOrNumArr = ToArray<string | number>; // string[] | number[]

也就是说:

ts 复制代码
ToArray<string | number> === ToArray<string> | ToArray<number>

若想避免分布式行为,可以加上中括号:

ts 复制代码
type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;

type ArrOfStrOrNum = ToArrayNonDist<string | number>; // (string | number)[]

参考文献

相关推荐
Highcharts.js9 分钟前
可视化商用图表库对比,如何选择?
前端·javascript·学习·信息可视化·highcharts·前端可视化
TheITSea1 小时前
JavaScript Web APIs
开发语言·前端·javascript
恋猫de小郭1 小时前
Android CLI 支持 AI Agent 通过 Device Streaming 调试云真机
android·前端·flutter
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-前端dist部署与Nginx反向代理rewrite
java·前端·nginx
鬓戈1 小时前
Claude Code frontend-design 插件调研 与 Vue 旧系统风格一致性方案
前端·vue.js·人工智能
IT_陈寒2 小时前
SpringBoot自动配置的坑我帮你踩过了
前端·人工智能·后端
其实防守也摸鱼2 小时前
网安自测题:掌握核心知识点的实用练习
linux·运维·服务器·前端·数据库·sql·xss
geovindu4 小时前
css: Timeline scribble
前端·css·iphone
沙漠之主12 小时前
C++编程教学设计资料:从入门到实战的完整课程方案
java·前端·c++
hasty12 小时前
不上传新包,也能改变用户拿到的版本:npm dist-tag 的 OIDC 权限治理
前端·npm·node.js