- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
本篇技术指南以 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 | 2typeof 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>>>, ]逐条分析,这些用例覆盖了返回类型提取的关键场景:
- 普通类型:
() => string应提取出string,() => 123应提取出字面量类型123; - 复杂对象:
() => 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 : 2fn1额外带有一个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
相关推荐
3步攻克Type Challenges函数返回类型提取
3步攻克Type Challenges函数返回类型提取 你是否在TypeScript项目中遇到过需要提取函数返回类型的场景?面对复杂的函数类型,不知道如何准确获
示例工程TypeScript 函数返回类型推断(Type from Func Return):从实现推导返回值类型
TypeScript 函数返回类型推断(Type from Func Return):从实现推导返回值类型 导读:本文聚焦 TypeScript 中"根据函数实
文档教程TypeScript 函数返回值类型推断:从 The Concise TypeScript Book 的 "Type from Func Return" 到 ReturnType 与条件类型提取
TypeScript 函数返回值类型推断:从 The Concise TypeScript Book 的 "Type from Func Return" 到 R
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考