
示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载导读本篇指南围绕 type-challenges 中等难度第 20 题 Promise.all作者 Anthony Fu标签array、promise展开目标是为一个接收「Promise 或 PromiseLike 对象数组」的函数PromiseAll补全类型签名使其返回值被精确推导为PromiseT其中T是每个输入解析结果按位置一一对应的数组。读完本文你将掌握条件类型 递归分发推导 Promise 解析结果的核心套路理解as const、Awaited、元组/数组不同分发语义对类型推导的影响并能直接对照本仓库的测试用例验证你的解法。题目原貌与目标题目要求用一句话概括即给函数PromiseAll指定类型它接受元素为 Promise 或类似 PromisePromiseLike的对象的数组返回值应为PromiseT其中T是这些 Promise 结果组成的数组。题目给出的起点模板位于 template.ts是一个完全未做类型约束的占位声明declare function PromiseAll(values: any): any显然any会完全丢失类型信息无论传入什么返回值都是any调用方拿不到任何推导结果。我们的任务就是把这个签名改写为declare function PromiseAllT extends readonly unknown[]( values: [...T] ): Promise{ [K in keyof T]: AwaitedT[K] }题面还给出了一个期望推导的示例见 README.md 与 README.zh-CN.mdconst promise1 Promise.resolve(3); const promise2 42; const promise3 new Promisestring((resolve, reject) { setTimeout(resolve, 100, foo); }); // 应推导出 Promise[number, 42, string] const p PromiseAll([promise1, promise2, promise3] as const)注意这里的两个关键细节promise2 42是普通值而非 Promise说明题目要求对「Promise 或 PromiseLike 的数组」中的非 Promise 元素原样保留调用处使用了as const把数组字面量固化为只读元组从而保留每个元素的位置与字面量类型最终推导结果才是Promise[number, 42, string]这样的元组形态而不是被放宽为Promise(number | string)[][]之类的联合数组。题目元数据可在 info.yml 中确认difficulty: mediumtags: [array, promise]作者为 Anthony Fu。前置知识从Awaited说起在给出完整解法之前先铺垫一个本仓库中同样关键的配套知识点——Awaited即 00189-easy-awaited 一题的MyAwaited。它是本题的核心「拆包」工具负责把PromiseT递归地解析为T。参考该题的测试用例 test-cases.ts可以看到MyAwaited需要处理的能力边界type X Promisestring type Y Promise{ field: number } type Z PromisePromisestring | number // 多层嵌套 type Z1 PromisePromisePromisestring | boolean type T { then: (onfulfilled: (arg: number) any) any } // thenable 对象 type cases [ ExpectEqualMyAwaitedX, string, ExpectEqualMyAwaitedY, { field: number }, ExpectEqualMyAwaitedZ, string | number, ExpectEqualMyAwaitedZ1, string | boolean, ExpectEqualMyAwaitedT, number, ]从中可以提炼出三个能力要求递归Z1是三层嵌套需要不断解包直到最内层PromiseLike 兼容T只是拥有then方法的 thenable 对象并非真正的Promise实例也要能解析出onfulfilled的参数类型联合分发string | number这样的联合类型需要被逐一解包后再合并。TypeScript 内置的AwaitedT正是官方提供的「递归解包」类型TS 4.5其行为与上面测试完全一致。它基于条件类型的可分配性判断实现type AwaitedT T extends null | undefined ? T // 保留 null / undefined : T extends object { then(onfulfilled: infer F): any } ? F extends ((value: infer V, ...args: any) any) ? AwaitedV // 取出 resolve 值并递归 : never : T // 非 thenable原样返回条件类型T extends ... ? ... : ...对裸类型参数具有分发distributive特性当T是联合类型时会分别对每个成员求值再合并结果这正是Awaitedstring | number能正确返回string | number的原因。解法一基于内置Awaited的映射类型利用 TypeScript 内置工具类型与映射类型mapped type可以写出最简洁的标准答案declare function PromiseAllT extends readonly unknown[]( values: [...T] ): Promise{ [K in keyof T]: AwaitedT[K] }逐步拆解组成部分作用T extends readonly unknown[]泛型约束入参必须是数组或只读数组as const产生只读元组values: [...T]展开T为元组让 TS 将字面量数组推导为「按元素逐一保存」的元组类型而非普通数组{ [K in keyof T]: ... }映射类型保持元组的顺序与长度逐个处理每个位置的元素类型AwaitedT[K]对第K个位置的类型递归解包 Promise / PromiseLikePromise...外层包裹模拟真实Promise.all的返回值形态为什么必须写成values: [...T]因为如果直接写values: TTypeScript 对数组字面量默认会推断为普通数组number[]会丢失「第几个元素是什么类型」的信息而可变元组展开[...T]会强制把实参按元组语义匹配T让T保留每个位置的精确类型。这正是题面示例中as const与[...T]相互配合的意义。再看映射类型为什么用keyof T而非T[number]元组的keyof T是0 | 1 | 2这样的数字字面量键集合映射后仍产出等长的元组若用T[number]则会退化为联合类型丢失位置信息。解法二条件类型 递归的手写实现如果不用内置Awaited也可以借助infer与递归自己「拆包」理解底层原理。先写一个简易版递归拆包type MyAwaitedT T extends PromiseLikeinfer R ? R extends PromiseLikeany ? MyAwaitedR // 仍是 PromiseLike继续递归 : R // 拆到底了 : T // 非 PromiseLike原样返回然后在PromiseAll中逐位置应用type MapAwaitedT extends readonly unknown[] { [K in keyof T]: T[K] extends PromiseLikeinfer R ? MyAwaitedR : T[K] } declare function PromiseAllT extends readonly unknown[]( values: [...T] ): PromiseMapAwaitedT这里核心是infer的用法在条件类型的分支中通过infer R声明一个待推断的类型变量让 TypeScript 从PromiseLikeinfer R中提取出R再配合递归处理多层嵌套。它验证了本题的本质——题目并不要求我们重新实现 Promise 语义而是用类型系统模拟「递归解包」这一运算。需要注意的是手写版本的分发语义与内置Awaited在边界细节上如null/undefined的保留、never的处理可能不完全一致因此生产代码中优先使用官方Awaited更稳妥。用测试用例验证解法本仓库为每一题都配备了可运行的测试文件。本题的测试位于 test-cases.ts共覆盖 5 个典型场景import type { Equal, Expect } from type-challenges/utils const promiseAllTest1 PromiseAll([1, 2, 3] as const) const promiseAllTest2 PromiseAll([1, 2, Promise.resolve(3)] as const) const promiseAllTest3 PromiseAll([1, 2, Promise.resolve(3)]) const promiseAllTest4 PromiseAllArraynumber | Promisenumber([1, 2, 3]) const promiseAllTest5 PromiseAll(number | Promisestring)[]([1, 2, Promise.resolve(3)]) type cases [ ExpectEqualtypeof promiseAllTest1, Promise[1, 2, 3], ExpectEqualtypeof promiseAllTest2, Promise[1, 2, number], ExpectEqualtypeof promiseAllTest3, Promise[number, number, number], ExpectEqualtypeof promiseAllTest4, Promisenumber[], ExpectEqualtypeof promiseAllTest5, Promise(number | string)[], ]逐条解读这 5 个用例它们恰好覆盖了本体的全部难点纯字面量 as const[1, 2, 3] as const得到只读元组readonly [1, 2, 3]映射后保持每个元素的字面量类型结果是Promise[1, 2, 3]。这要求T约束必须允许只读数组。混合 Promise 与普通值 as constPromise.resolve(3)被解包为number普通值1、2原样保留得到Promise[1, 2, number]。不加as const的混合数组数组字面量被放宽为(number | Promisenumber)[]T退化为数组而非元组映射后得到number[]最终是Promisenumber[]。注意这里Promise.resolve(3)并未解包为3的字面量而是先与number合并再统一解包这正是「无as const时元素类型被提升」的典型表现。显式指定泛型参数为Arraynumber | Promisenumber结果应为Promisenumber[]说明泛型声明必须允许调用方显式传入数组类型参数。显式指定(number | Promisestring)[]number原样保留、Promisestring解包为string合并后得到(number | string)[]最终是Promise(number | string)[]。这验证了「联合类型中的每个成员各自按规则处理」的分发能力。这些测试借助 utils/index.d.ts 中导出的Equal类型做严格相等校验其实现为基于函数参数逆变位置的经典等价性判断export type EqualX, Y (T() T extends X ? 1 : 2) extends (T() T extends Y ? 1 : 2) ? true : false由于Equal对any与具体类型的区分非常敏感只要PromiseAll的推导结果与期望类型存在任何细微差异例如返回了any、丢失了字面量、顺序错乱测试就会以类型错误的形式暴露出来。这也是本仓库所有题目共用的验证机制——你可以在任意一题的test-cases.ts顶部看到import type { Equal, Expect } from type-challenges/utils这一行例如 00189-easy-awaited/test-cases.ts 中的MyAwaited测试。解题思路总结从现象到本质把测试用例与两种解法放在一起可以提炼出解这道题的思维主线先约束入参形态T extends readonly unknown[]兼容普通数组与as const只读元组再用[...T]保位置让 TypeScript 按元组语义匹配实参保留每个位置的独立类型然后用映射类型逐位处理{ [K in keyof T]: AwaitedT[K] }保持元组结构只替换每个元素的类型最后外层包Promise...与原生Promise.all的返回形态保持一致。一句话记住本题「先约束、再展开、逐位解包、整体包 Promise」。而解包这一行为本身就是 00189-easy-awaited 中的Awaited所训练的能力——两道题在知识链上是前后衔接的Awaited是这道Promise.all题的前置技能。延伸思考若不写[...T]而直接写values: TPromiseAll([1, 2, 3])会推导出Promisenumber[]而不是Promise[1, 2, 3]位置信息丢失——可以自己动手对比验证若要支持「某个位置是never」或嵌套更深PromisePromisePromise...的情况内置Awaited已自动递归处理无需额外代码本题只关心「入参数组的解析结果」与运行时Promise.all的并发、失败聚合等语义无关——类型层永远无法也不应该模拟运行时行为这是编写类型工具时的重要边界意识。完成解法后可回到仓库根目录 README.md 或中文总览 README.zh-CN.md 查看题目索引也可对照 README.ja.md 查看日文版题面。赞分享示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载相关推荐type-challenges 第 20 题 Promise.all为 PromiseAll 编写类型推导的完整解析type challenges 第 20 题 Promise.all为 PromiseAll 编写类型推导的完整解析 本篇指南以 type challenge示例工程type-challenges 第 20 题为 Promise.all 编写精确的类型签名数组与 Promise 解包type challenges 第 20 题为 Promise.all 编写精确的类型签名数组与 Promise 解包 导读 本篇围绕 type chal示例工程type-challenges 中等题精解用模板字面量类型实现 CapitalizeTtype challenges 中等题精解用模板字面量类型实现 CapitalizeT 本篇文章基于开源仓库 type challengesCollect示例工程上一篇MCprep插件深度解析Blender中高效制作Minecraft动画的实用指南下一篇高性能安卓日历组件架构方案NCalendar的可扩展时间管理技术实现创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考