像三元表达式一样写类型?深入理解 TS 条件类型与 `infer` 推断

📝 引言

📌 场景/痛点

在高级类型编程中,我们经常面临"逆向"的需求:

  • 有一个函数类型 () => User,怎么把 User 这个返回值类型单独取出来用?
  • 有一个 Promise<number>,怎么直接拿到里面的 number 类型赋值给变量?

初学者可能会复制粘贴 User 的定义,但如果返回值变了,你就得改两个地方。

我们需要的不是"重新定义",而是"提取"。 就像正则提取文本一样,TypeScript 提供了 infer 关键字来帮我们从现有类型中推断出我们要的部分。

✨ 最终效果

掌握 infer 后,你将能随心所欲地从复杂类型中剥离出你需要的子类型。

📖 内容概览

本文将带你掌握类型逻辑的"判断"与"提取":

  1. 条件类型T extends U ? X : Y 的基本用法。
  2. 核心神器 infer:理解"待推断类型变量"。
  3. 实战演练 :手写 ReturnTypeDeepPromise
  4. 分布式条件类型:理解联合类型在条件下的自动分发。

🛠️ 正文

1. 环境准备

继续在 src/ 下新建 conditionals.ts。推荐使用 TypeScript Playground 进行调试,方便实时查看推导结果。

2. 条件类型基础

条件类型的语法和 JavaScript 的三元运算符完全一致。它作用于类型层面。

typescript 复制代码
// 定义一个"非空"类型
type NonNullable<T> = T extends null | undefined ? never : T;

type T1 = NonNullable<string>; // string
type T2 = NonNullable<null>;   // never

核心逻辑 :检查 T 是否能够赋值给 null | undefined,如果是,返回 never,否则返回 T

3. 核心魔法:infer 关键字

infer 是 "Infer"(推断)的缩写。它只能在 extends 条件语句的 true 分支中使用。它的作用是:声明一个类型变量,让 TS 自动去匹配并填充它。

3.1 提取函数返回值

让我们来复刻 TS 内置的 ReturnType 工具类型。

typescript 复制代码
// 定义:如果 T 是一个函数,返回值类型记为 R,那就返回 R,否则返回 never
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// 测试
function getUser() {
    return { id: 1, name: "Alice" };
}

type User = MyReturnType<typeof getUser>;
// 推导结果:{ id: number; name: string; }

发生了什么?

TS 看到 getUser 的类型是 () => { id: number; name: string; },它尝试和 (...args: any[]) => infer R 匹配。既然能匹配上,它就自动把 R 填充为返回值的类型。

3.2 提取数组元素类型
typescript 复制代码
type Head<T> = T extends [infer H, ...any[]] ? H : never;

type List = [string, number, boolean];
type FirstElement = Head<List>; // string

4. 实战:深度解包 Promise

在异步开发中,我们经常遇到 Promise<Promise<Data>> 这种嵌套结构。TS 已经内置了 Awaited,我们来手写一个 DeepPromise 来理解原理。

typescript 复制代码
type DeepPromise<T> = T extends Promise<infer U>
    ? DeepPromise<U> // 递归:如果发现是 Promise,就继续往里剥
    : T; // 如果不是,直接返回 T

// 测试
type P1 = Promise<string>;
type P2 = Promise<Promise<number>>;

type R1 = DeepPromise<P1>; // string
type R2 = DeepPromise<P2>; // number (递归成功!)

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

这是一个非常重要的特性。当被检测的类型 T 是一个联合类型(如 A | B)时,条件类型会被分发(应用到每一个成员)。

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

// T = string | number

// 🔄 分布式过程:
// 1. string extends any ? string[] : never  -> string[]
// 2. number extends any ? number[] : never  -> number[]
// 3. 结果合并:string[] | number[]

type Result = ToArray<string | number>;
// 结果:string[] | number[] (注意:不是 (string | number)[])

如何阻止分发?

只需要用方括号 [] 把 T 包起来即可 [T]

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

❓ 常见问题

Q1: 报错 infer declarations are only permitted in the extends clause of a conditional type

A: 这个报错很明显:你只能在 T extends ... 后面的分支里用 infer。你不能直接写 type Foo = infer R;

Q2: infer 可以推断多个值吗?

A: 可以!你可以声明多个 infer 变量。

typescript 复制代码
type TwoArgs = T extends (a: infer A, b: infer B) => any ? [A, B] : never;

type T = TwoArgs<(x: string, y: number) => void>;
// 结果: [string, number]

Q3: 为什么我的 DeepPromise 会报错"类型递归过深"?

A: 如果循环引用太严重,TS 可能会放弃计算。这通常是因为类型定义结构极其复杂。在实战中,TS 内置的 Awaited 已经做了最大程度的优化,尽量使用内置工具,自己手写递归要注意逻辑闭环,确保有终止条件。


🎯 总结

条件类型与 infer 是 TypeScript 类型系统的灵魂。本文重点掌握了:

  1. 条件类型T extends U ? X : Y 的类型逻辑判断。
  2. infer :在 extends 中声明"占位变量",让 TS 自动提取类型。
  3. 实战应用 :手写 ReturnType 和递归解包 Promise

🚀 下期预告:

学会了提取类型,但有时候提取出来的类型太宽泛了,或者 TS 推断得太"聪明"反而出了错。

下一篇文章我们将深入了解 《TS 5.x 必备神器:satisfiesNoInfer,掌握控制类型推导的最新算子。

💬 互动环节:

你以前在写类型体操时,有没有被 infer 搞晕过?学会之后是不是觉得简单多了?

如果觉得文章对你有帮助,请点赞👍、收藏⭐、关注👀,我们下期继续挑战高阶技巧!

相关推荐
a11177619 分钟前
程序化几何背景生成器(html 开源)
前端·开源·html
浮笙若有梦34 分钟前
我开源了一个比 Ant Design Table 更好用的高性能虚拟表格
前端·vue.js
一只程序熊39 分钟前
vite-cool-unix-ctx] Unexpected token l in JSON at position 0
java·服务器·前端
张元清1 小时前
React Hooks vs Vue Composables:2026 年全面对比
前端·javascript·面试
yuki_uix1 小时前
从三个自定义 Hook 看 React 状态管理的设计思想
前端·javascript
大漠_w3cpluscom1 小时前
如何在 clamp() 中使用 auto 值
前端·css·html
Younglina1 小时前
🏸 从零打造一个羽毛球球线追踪网站:纯前端实战指南
前端
C澒1 小时前
微前端容器标准化:从碎片化到统一架构的渐进式改造
前端·架构
CyrusCJA1 小时前
JavaScript原型与super关键字
前端·javascript·js
左耳咚1 小时前
Claude Code 技术全景概览
前端·ai编程