☰
TypeScript 类型挑战 2:不使用内置 ReturnType 手写函数返回类型提取(infer 实战指南)
2026/9/30 1:51:14 网站建设 项目流程
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

本文围绕 type-challenges 仓库中的00002-medium-return-type(Get Return Type)挑战展开,完整还原题目要求:不借助 TypeScript 内置ReturnType<T>工具类型,独立实现一个能从函数类型中提取返回类型的MyReturnType<T>。通过拆解条件类型与infer关键字的配合原理,并结合本仓库的template.ts起始模板与test-cases.ts测试用例逐条验证,读者将掌握类型级"模式匹配"的核心心法,并顺带打通Parameters<T>、Awaited<T>等同类工具类型的实现思路。

挑战概览:题目是什么

该挑战位于 questions/00002-medium-return-type,是 type-challenges 中编号#2的中等难度(medium)题目。根据其 info.yml 元数据,它的标签为infer与built-in,由作者 Anthony Fu 提供。

题目原文要求只有一句话:

Implement the built-inReturnType<T>generic without using it.(不使用ReturnType实现 TypeScript 的ReturnType<T>泛型。)

即:我们需要自己写一个MyReturnType<T>,使其行为与内置的ReturnType<T>一致——输入一个函数类型,输出该函数声明(或推断)的返回值类型。

题目给出的核心示例:

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

注意这里的fn没有显式标注返回类型,1和2会被 TypeScript 收窄为字面量类型,因此typeof fn的返回类型是1 | 2联合类型,MyReturnType<typeof fn>必须原样输出这个联合类型。

起步模板 template.ts 中只有一个占位实现:

type MyReturnType<T> = any

我们的任务就是把any替换成真正的类型逻辑,并通过全部测试用例。

先理解目标:内置 ReturnType 到底做了什么

ReturnType<T>是 TypeScript 标准库中预置的实用工具类型(utility type),其作用是从一个函数类型中取出返回类型。例如:

type R1 = ReturnType<() => string> // string type R2 = ReturnType<() => 123> // 123 type R3 = ReturnType<() => () => 'foo'> // () => 'foo'

它在日常开发中常用于从已有函数推断结果类型,例如把某个 API 函数的返回值类型提取出来复用。本题正是要求我们用类型编程的方式"复刻"它,从而理解它背后的实现机制——也就是infer关键字。

解法拆解:条件类型 + infer 提取返回类型

第一步:约束泛型必须是一个函数

内置ReturnType<T>的泛型约束是T extends (...args: any) => any,即只接受函数类型。我们手写时也可以先加上同样的约束,让不合法输入在编译期就报错:

type MyReturnType<T extends (...args: any) => any> = any

其中(...args: any) => any是"任意函数"的类型写法:参数可以是任意数量和任意类型(用 rest 参数 +any表示),返回任意类型。用(...args: any[]) => any也是常见写法,两者在本题场景下等价。

第二步:用条件类型做"模式匹配"

TypeScript 的条件类型语法形如T extends U ? X : Y,它的强大之处在于:当T与U匹配时,可以在U中用infer声明一个"待推断的类型变量",由编译器根据T的实际结构自动填充。

函数类型(...args: any) => infer R中,infer R表示"返回值类型暂时未知,请编译器从T中推断出来"。于是核心解法如下:

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

逐段解释:

片段作用
T extends (...args: any) => any(外层)泛型约束:只接受函数类型
T extends (...args: any) => infer R(条件判断)匹配任意函数结构,同时把返回值类型"绑定"到R
? R匹配成功:返回推断出的返回类型
: any兜底分支:由于约束已保证是函数,实际不会走到

第三步:验证题目示例

回到题目的fn:

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

typeof fn是(v: boolean) => 1 | 2。把它代入条件类型后,编译器在(...args: any) => infer R中匹配成功,并将R推断为1 | 2——与题目要求完全一致。这正是条件类型"分发式推断"的体现:函数签名的返回位置有多少种字面量可能,infer R就收拢成多大的联合类型。

源码级验证:对照测试用例逐条检查

在 test-cases.ts 中,本挑战共准备了 7 组测试断言:

import type { Equal, Expect } from '@type-challenges/utils' 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>>>, ] type ComplexObject = { a: [12, 'foo'] bar: 'hello' prev(): number }

逐条分析我们的解法如何通过:

  • () => string→string:最朴素的函数返回类型。
  • () => 123→123:字面量类型被完整保留,而不是被放大为number。
  • () => ComplexObject→ComplexObject:返回复杂对象结构(含数组、字符串字面量属性和方法prev(): number)时,infer R会原样提取整个对象类型。
  • () => Promise<boolean>→Promise<boolean>:注意infer R只剥离一层函数返回类型,不会递归解包 Promise 内部类型(那是另一道题 00189-easy-awaited 的任务)。
  • () => () => 'foo'→() => 'foo':返回类型本身是一个函数类型,infer R能正确推断出"返回函数的函数"的返回类型。
  • typeof fn→1 | 2:无显式返回注解、基于条件分支的返回被推断为字面量联合。
  • typeof fn1→1 | 2:fn1形如(v: boolean, w: any) => v ? 1 : 2,带有两个参数,证明infer R的提取只关注返回值位置,与参数数量、参数类型无关。

这里用到的断言工具Equal与Expect定义在 utils/index.d.ts:

export type Expect<T extends true> = T export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false

Expect<T extends true>要求传入的必须是true类型,否则该行声明会编译报错;Equal<X, Y>则用"函数返回类型协变比较"的技巧做严格相等判断,能够区分1与number这类可互相赋值但不等价的结构。因此,只要MyReturnType的结果与期望类型哪怕有一丁点差别(比如把123放大成了number),整个测试文件都会编译失败——这就是本挑战"在线判题"的底层机制。

边界与陷阱:为什么不能写成其他形式

不能直接用T的索引访问

有人会想:函数类型是对象结构,能不能用T['return']之类的方式取返回类型?TypeScript 的函数类型并不暴露可索引的属性键,这种写法无法成立;函数签名的拆解正是infer的用武之地。

兜底分支别用never?

条件类型写成: any与: never都能通过本题测试(因为泛型约束保证T必为函数,真分支恒成立)。但内置ReturnType的兜底是any,保持一致更贴近"复刻内置工具类型"的题目语义;写成never在约束被放宽时会产生不同的行为,属于细节差异,了解即可。

参数位置用any而非具名参数

(...args: any) => any中参数用 rest +any是最宽松的匹配方式,确保任意签名的函数都能匹配上。如果写成(arg: number) => infer R,那么返回类型相同但参数不同的函数就会落入兜底分支,导致类型错误。

举一反三:同类 infer 模式的关联挑战

本题的infer技巧在 type-challenges 中反复出现,掌握它可以顺势解决一系列题目:

  • 03312-easy-parameters:把infer R挪到参数位置(...args: infer P) => any,即可提取函数参数元组,这正是内置Parameters<T>的实现。
  • 00189-easy-awaited:infer R配合递归(Awaited<R>),实现Promise类型的逐层解包。
  • 00010-medium-tuple-to-union:在数组/元组类型上使用T[number]等手段将元组展开为联合。

它们共通的思维方式是:把"类型结构是否匹配"表达为条件类型,用infer在匹配处声明变量,让编译器替你完成推断。本题是这条思维链上最基础、最干净的一环。

本地运行与验证

type-challenges 支持在本地 IDE 中做题与校验。根据仓库根目录 README.md 的 "Play Locally" 说明,前提是安装最新版 Node.js 与 pnpm,然后:

pnpm install

随后运行生成脚本(对应根 package.json 中的pnpm generate):

pnpm generate

生成的练习文件即可在支持 TypeScript 的编辑器里编写MyReturnType,并通过编译器对test-cases.ts的静态检查来验证答案。需要留意的是,全部挑战均在strict 模式下运行(见根 README.md 的说明,以及 tsconfig.base.json 中开启的"strict": true),因此解题过程中涉及的可空性、类型收窄等行为都会按严格模式判定。

如果想直接在本仓库查看本题素材,可以从 template.ts(起始模板)出发,用上面的解法替换any,再对照 test-cases.ts 确认全部断言通过;中文题目说明可参考 README.zh-CN.md。

小结

MyReturnType<T>的完整实现只有三行:

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

它背后承载了三个必须内化的知识点:泛型约束限定输入范围、条件类型提供结构匹配的分支语义、infer声明"由编译器推断的占位类型变量"。三者叠加,就让 TypeScript 的类型系统具备了从复杂类型中"拆出"任意组成部分的能力。以此题为起点,再去挑战Parameters、Awaited乃至更复杂的类型递归,都将水到渠成。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:claude-seo 语义主题聚类实战:用 SERP 重叠数据驱动 Hub-and-Spoke 内容架构规划
下一篇:docopt.rs高级技巧:自定义类型解码与错误处理最佳实践

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询