news 2026/9/30 8:13:27

在 type-challenges 中实现 MyReturnType:用 infer 条件类型提取函数返回类型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 type-challenges 中实现 MyReturnType:用 infer 条件类型提取函数返回类型
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

本篇技术指南以 type-challenges 仓库的第 00002 号中等难度题目「获取函数返回类型(Get Return Type)」为主线,完整讲解如何在不使用内置ReturnType<T>的前提下,借助条件类型与infer关键字自行实现返回类型提取工具。读完本文,你将掌握infer在类型推断中的核心用法、函数类型解构的基本套路,并学会如何对照仓库中的测试用例(test-cases)验证自己的类型实现。

一、题目解读:什么是 Get Return Type

本题出自 questions/00002-medium-return-type/README.zh-CN.md,作者是 Anthony Fu,标签为#infer与#built-in,难度为中等(medium)。题面要求非常明确:

不使用ReturnType实现 TypeScript 的ReturnType<T>泛型。

也就是说,我们要基于 TypeScript 内置ReturnType的行为规范,从零实现一个同功能的类型工具MyReturnType<T>,其输入是一个函数类型,输出是该函数的返回值类型。

题目给出的参考示例:

const fn = (v: boolean) => { if (v) return 1 else return 2 } type a = MyReturnType<typeof fn> // 应推导出 "1 | 2"

这里的fn是一个接受布尔参数、可能返回1或2的函数,因此typeof fn的函数返回类型是联合类型1 | 2。MyReturnType<typeof fn>必须同样推导出"1 | 2"(README 中用字符串形式的"1 | 2"描述期望结果)。

题目被打上#built-in标签,是因为它要求我们"重新发明"一个 TypeScript 标准库中已经存在的工具类型;而#infer标签则直接点明了核心解题武器——条件类型中的infer关键字。

二、从模板开始:理解起点代码

每个 type-challenges 题目都在其目录下提供template.ts,这是挑战者的起点。本题的模板位于 questions/00002-medium-return-type/template.ts,内容极简:

type MyReturnType<T> = any

也就是说,初始状态下MyReturnType<T>对任何类型都返回any,类型检查会因与期望类型不符而失败。你的任务就是把any替换为真正能从T中提取返回类型的类型运算。

需要注意的是,模板中并没有约束T必须是函数类型。这意味着我们的实现要足够健壮:要么通过约束T extends (...args: any[]) => any将输入限定为函数,要么借助条件类型在非函数输入时落入never分支(仓库其余题目的实现风格也普遍采用这种"先判断后提取"的写法)。

三、核心原理:条件类型与 infer

3.1 条件类型(Conditional Types)

条件类型的语法形如:

type IsString<T> = T extends string ? true : false

当T可以赋值给string时结果为true,否则为false。条件类型的分支中不仅可以使用"字面量类型"作为判断目标,也可以使用"带占位符的抽象模式"来捕获子类型——这正是infer登场的地方。

3.2 infer:在条件类型中声明推断变量

infer允许我们在extends的右侧声明一个类型变量,TypeScript 编译器会根据实际传入的类型自动推断出该变量的具体值。最经典的例子是从数组类型中提取元素类型:

type ElementType<T> = T extends (infer U)[] ? U : never

当T是string[]时,infer U被推断为string;当T是number[]时,U为number。

函数类型同样可以解构。函数类型可以写作(...args: any[]) => R的形式,因此我们可以用:

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

来提取函数返回类型。infer R位于函数类型的返回值位置,编译器会把实际函数的返回类型赋给R。

3.3 完整解答

将上述思路写入模板,得到本题的完整实现:

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never

对于题目的示例:

const fn = (v: boolean) => v ? 1 : 2 type a = MyReturnType<typeof fn> // 1 | 2

typeof fn是(v: boolean) => 1 | 2,infer R被推断为1 | 2,因此a正确得到联合类型1 | 2。

把答案写回 questions/00002-medium-return-type/template.ts,即可替换原来的any。

四、测试用例:题目如何验证你的答案

4.1 测试文件结构

本题的验证逻辑位于 questions/00002-medium-return-type/test-cases.ts。文件首先导入了仓库统一的类型测试工具:

import type { Equal, Expect } from '@type-challenges/utils'

这两个工具定义在 utils/index.d.ts 中:

  • Expect<T extends true>:要求传入的泛型参数必须为true,否则类型检查直接报错;
  • Equal<X, Y>:通过一个巧妙的函数类型比对技巧((<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2))严格判断两个类型是否完全相等,可区分any、联合类型、字面量类型等细微差别。

测试的原理是:Equal<期望类型, MyReturnType<输入函数>>为true,再交给Expect校验。任何一处推断错误(例如返回any、漏掉联合分支)都会导致类型检查失败。

4.2 逐条解读测试用例

type cases = [ Expect<Equal<string, MyReturnType<() => string>>>, Expect<Equal<123, MyReturnType<() => 123>>>, Expect<Equal<ComplexObject, MyReturnType<() => ComplexObject>>>, Expect<Equal<Promise<boolean>, MyReturnType<() => Promise<boolean>>>>, Expect<Equal<() => 'foo', MyReturnType<() => () => 'foo'>>>, Expect<Equal<1 | 2, MyReturnType<typeof fn>>>, Expect<Equal<1 | 2, MyReturnType<typeof fn1>>>, ]

逐条分析,这些用例覆盖了返回类型提取的关键场景:

  1. 普通类型:() => string应提取出string,() => 123应提取出字面量类型123;
  2. 复杂对象:() => ComplexObject应提取出ComplexObject,其中:
type ComplexObject = { a: [12, 'foo'] bar: 'hello' prev(): number }

验证了提取器对嵌套数组[12, 'foo']、字符串字面量'hello'、方法签名prev(): number等复杂结构类型依然原样返回; 3.泛型类型:() => Promise<boolean>提取出Promise<boolean>,说明infer R捕获的是完整返回类型本身,不做任何拆解; 4.返回函数类型:() => () => 'foo'提取出() => 'foo',即返回类型可以是函数类型(这也暗示了"链式提取"的可能性——若想提取嵌套函数的最终返回类型,需要递归地再次对R应用MyReturnType); 5.联合类型返回:typeof fn与typeof fn1均应提取出1 | 2,其中:

const fn = (v: boolean) => v ? 1 : 2 const fn1 = (v: boolean, w: any) => v ? 1 : 2

fn1额外带有一个any参数,用于确认参数的数量与类型不会影响返回类型提取。

4.3 测试如何被执行

type-challenges 的题目通过tsc类型检查来判定对错:只要test-cases.ts中Expect<Equal<...>>的所有断言都通过,即视为挑战成功。你可以在本地使用仓库根目录的 package.json 中锁定的 TypeScript(^5.3.3)对该文件做纯类型检查(tsc --noEmit)来验证你的template.ts实现。

五、实现细节与边界讨论

5.1 为什么用(...args: any[])而非(...args: unknown[])

参数类型写成any[]是为了让任何函数都能匹配extends左侧的模式。若改用unknown[],某些带有特定参数要求的函数在严格模式下可能无法通过可赋值性判断,导致落入never分支。这也是内置ReturnType与绝大多数社区实现采用的通用写法。

5.2 非函数输入的行为

由于条件类型具备"未命中则走 false 分支"的特性,我们的实现T extends (...args: any[]) => infer R ? R : never在T不是函数时(例如MyReturnType<string>)会得到never,这是一种合理且可预期的降级行为。若希望强制约束,也可写作:

type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never

但这会牺牲对非函数类型传入时的容错性,两种风格各有取舍。

5.3 infer 的其他应用线索

本题标签中的#infer提示我们:infer的能力远不止提取返回类型。同样的模式可以用于提取函数参数列表(等价于内置Parameters<T>)、提取Promise内部类型(等价于内置Awaited<T>的雏形)、从模板字符串类型中提取字面量子串等。仓库中大量其他题目(如00300-hard-string-to-number、00114-hard-camelcase)都是对infer在字符串字面量层面应用的延伸,完成本题是掌握这一核心技能的起点。

六、小结

本题的核心收获可以概括为一句话:在条件类型的extends模式中使用infer R占位函数返回值位置,即可让 TypeScript 编译器替你完成返回类型推断。对照 test-cases.ts 的七组断言,MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never这一行代码同时满足普通类型、字面量、复杂对象、泛型类型、函数返回值与联合类型六类场景,这也是为什么它成为条件类型与infer学习路径中不可绕过的经典入门题。

如果你想继续深入,可以回到 README.zh-CN.md(仓库根目录的官方中文总览)查看完整题目列表,或参考根目录 README.zh-CN.md 了解整个挑战项目的组织方式;infer的进阶应用还可以结合仓库 guides/infer.md 的学习材料(注:当前仓库中该指南尚未完成)以及utils工具库 utils/index.d.ts 中Equal、Expect等测试辅助类型的实现来加深理解。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:OneUptime 与 Slack 集成指南:从通知配置到自托管网络架构
下一篇:IntelliJ Platform 性能实验 Jupyter Notebook 编写规范:基于 uv 与 nb.py 构建可复现实验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 8:12:14

大数据预处理全攻略:从数据清洗到工程化实践

1. 为什么数据预处理是大数据项目的隐形地基1.1 数据预处理到底是什么我入行大数据这么多年&#xff0c;有个感受越来越深&#xff1a;真正决定一个项目成败的&#xff0c;往往不是算法模型有多高级&#xff0c;也不是集群规模有多大&#xff0c;而是最不起眼的那一步——数据预…

作者头像 李华
网站建设 2026/9/30 8:12:14

EOS8.3.3附件删除权限控制:多上传人场景下只能删自己上传的

附件删除权限这件事&#xff0c;在EOS8.3.3上做过的朋友应该都懂那种纠结&#xff1a;需求一句话&#xff0c;“附件允许多选&#xff0c;不是同一个人上传的&#xff0c;只能删除自己上传的”&#xff0c;写起来却要拆出一整套逻辑。多附件、多上传人、前端的删除入口、后端的…

作者头像 李华
网站建设 2026/9/30 8:10:35

赫斯曼交换机命令行手册:从Console登录到VLAN与环网配置实操

简介&#xff1a;这份赫斯曼交换机命令行简易用户手册面向网络运维与工程实施人员&#xff0c;聚焦工业交换机在项目交付中的基础配置与配置文件上传场景&#xff0c;适合具备一定网络基础、需要快速上手命令行操作的读者。资源包共1个docx文档&#xff0c;约125KB&#xff0c;…

作者头像 李华
网站建设 2026/9/30 8:10:32

ESP-IDF组件开发核心原理与VS Code实践指南

1. 为什么在 VS Code 里“创建组件”不是点个按钮就完事&#xff1f;很多人第一次用 ESP-IDF 在 VS Code 里开发&#xff0c;看到官方文档里写着“创建新组件”&#xff0c;下意识就去菜单栏翻“File → New Component”——结果什么都没找到。我当年也是这样&#xff0c;在终端…

作者头像 李华