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

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

相关推荐
恋猫de小郭3 小时前
AGENTS.md 真的对 AI Coding 有用吗?或许在此之前你没用对?
前端·人工智能·ai编程
sunny_5 小时前
构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的
前端·rust·前端工程化
rfidunion6 小时前
springboot+VUE+部署(12。Nginx和前端配置遇到的问题)
前端·vue.js·spring boot
珹洺6 小时前
Java-servlet(五)手把手教你利用Servlet配置HTML请求与相应
java·运维·服务器·前端·servlet·html·maven
QQ24391976 小时前
语言在线考试与学习交流网页平台信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·spring boot·sql·学习·java-ee
范特西.i7 小时前
QT聊天项目(6)
前端
a1117767 小时前
水体渲染系统(html开源)
前端·开源·threejs·水体渲染
程序员小李白7 小时前
CSS 盒子模型
前端·css·html
Zzz不能停7 小时前
单行 / 多行文本显示省略号(CSS 实现)
前端·css
xiaoxue..7 小时前
TailwindCSS:从“样式民工”到“UI乐高大师”的逆袭
前端·css·ui