像三元表达式一样写类型?深入理解 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 搞晕过?学会之后是不是觉得简单多了?

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

相关推荐
沸点小助手1 小时前
「国产龙虾谁能打过OpenClaw & 你敢让微信龙虾碰代码吗」沸点获奖名单公示|本周互动话题上新🎊
前端·后端·面试
skywalk81631 小时前
请学习kotti的前端(kotti其实是没有分离的前端的)实现,做到形似kotti那样的前端页面。
前端·学习
UI设计兰亭妙微1 小时前
兰亭妙微加载体验设计白皮书:从骨架屏到后台加载的全场景优化策略
前端·b端界面设计·ui设计公司
逆光如雪1 小时前
移动端卡片边框怎么做高级?我用 CSS 实现了设计师的刁钻要求
前端·css·vue.js
scan7241 小时前
龙虾读取session历史消息
java·前端·数据库
莹宝思密达1 小时前
地图显示西安经济开发区边界线-2023.12
前端·vue.js·数据可视化
lizhongxuan1 小时前
LLM Wiki:让大模型替你打理知识库的完整指南
前端·后端·面试
神の愛1 小时前
利用json-to-ts工具进行转换,放置在typeScript.ts文件中
javascript·typescript·json
宇擎智脑科技2 小时前
Claude Code 源码分析(七):终端 UI 工程 —— 用 React Ink 构建工业级命令行界面
前端·人工智能·react.js·ui·claude code
dragon7252 小时前
Flutter错误处理机制
前端·flutter