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

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

相关推荐
雪芽蓝域zzs2 小时前
uniapp 省市区三级联动
前端·javascript·uni-app
Highcharts.js2 小时前
Next.js 集成 Highcharts 官网文档说明(2025 新版)
开发语言·前端·javascript·react.js·开发文档·next.js·highcharts
总爱写点小BUG2 小时前
探索 vu-icons:一款轻量级、跨平台的 Vue3 & UniApp SVG 图标库
前端·前端框架·组件库
晚霞的不甘2 小时前
Flutter for OpenHarmony手势涂鸦画板开发详解
前端·学习·flutter·前端框架·交互
We་ct2 小时前
LeetCode 73. 矩阵置零:原地算法实现与优化解析
前端·算法·leetcode·矩阵·typescript
晚霞的不甘2 小时前
Flutter for OpenHarmony 实现动态天气与空气质量仪表盘:从 UI 到动画的完整解析
前端·flutter·ui·前端框架·交互
~小仙女~2 小时前
组件的二次封装
前端·javascript·vue.js
这是个栗子2 小时前
AI辅助编程(一) - ChatGPT
前端·vue.js·人工智能·chatgpt
2501_944448002 小时前
Flutter for OpenHarmony衣橱管家App实战:预算管理实现
前端·javascript·flutter