ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用 TypeScript 类型系统实现动态参数柯里化:type-challenges 00462 Currying 2 深度解析

用 TypeScript 类型系统实现动态参数柯里化:type-challenges 00462 Currying 2 深度解析 示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载type-challenges 的第 00462 题Currying 2难度Extreme / 地狱级要求为「动态参数柯里化」函数DynamicParamsCurrying写出正确类型原函数可以一次接收全部参数也可以分批每批至少一个参数传入只要所有参数的类型与顺序一致最终必须精确返回原函数的返回值类型。本文以该题目的 README.md及 中文版为核心结合 template.ts 与 test-cases.ts 逐条拆解需求与测试用例并给出能够完整通过全部断言的类型级参考实现。读完本文你将掌握「变长元组前缀匹配 递归柯里化」这一在类型挑战中极具复用价值的核心套路。挑战总览项目内容挑战编号00462Currying 2难度extremeinfo.yml 中标记为difficulty: extreme作者Kimhubvue模板文件template.tsdeclare function DynamicParamsCurrying(fn: any): any测试文件test-cases.ts相关挑战00017 · Currying 1hard题目编号 00462 是 00017「Currying 1」的进阶版本info.yml 中related: 17。Currying 1 要求每次调用只接收一个参数而 Currying 2 允许每次调用一次性接收一至多个参数两者在类型实现上的复杂度差别很大。柯里化与「动态参数柯里化」题目对柯里化的定义是Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument.即将「一次接收多个参数的函数」转换为「一系列各自接收单个参数的函数」。但题目进一步指出在实际前端开发中参数个数动态化的柯里化反而更常见例如Function.bind(this, [...params])const func (a: number, b: number, c: number) { return a b c } const bindFunc func(null, 1, 2) const result bindFunc(3) // result: 6bind允许一次性绑定任意数量的参数剩余参数再在调用时补齐——这正是「动态参数柯里化」的运行期形态。因此题目在 Currying 1 的基础上要求为这种动态版本写出类型const add (a: number, b: number, c: number) a b c const three add(1, 1, 1) const curriedAdd DynamicParamsCurrying(add) const six curriedAdd(1, 2, 3) // 一次给满 3 个参数 const seven curriedAdd(1, 2)(4) // 先给 2 个再给 1 个 const nine curriedAdd(2)(3)(4) // 每次给 1 个共 3 次six、seven、nine的类型都应被推断为number即原函数add的返回值类型。顺带一提英文原文档中第三行示例变量名为nine2 3 4 9而中文版 README 中该处写作eight从计算结果看应属笔误以英文版nine为准。需求拆解三条硬性规则题目原文对DynamicParamsCurrying的约束可以拆解为以下三条输入函数参数个数可为零到多个DynamicParamsCurrying接受的函数可以有任意数量的参数包括零参数。柯里化后的函数每次调用至少接收一个参数与 Currying 1「每次恰好一个参数」不同这里每次调用允许传入一至多个参数但不能为空调用。分批方式自由但总量与顺序必须匹配所有调用传入的参数按顺序拼接后必须与原函数的参数列表完全一致一旦全部参数给齐就应返回原函数精确的返回值类型。这三条规则在测试文件中体现得非常明确。测试用例逐条解读测试文件 test-cases.ts 构造了两个待柯里化函数一个 3 参数函数和一个 7 参数函数const curried1 DynamicParamsCurrying((a: string, b: number, c: boolean) true) const curried2 DynamicParamsCurrying((a: string, b: number, c: boolean, d: boolean, e: boolean, f: string, g: boolean) true)3 参数函数的三种调用方式const curried1Return1 curried1(123)(123)(true) // 每次 1 个参数 const curried1Return2 curried1(123, 123)(false) // 先 2 个再 1 个 const curried1Return3 curried1(123, 123, true) // 一次 3 个三种写法都合法且curried1Return1/2/3的类型都必须精确等于boolean。7 参数函数的十种分批模式const curried2Return1 curried2(123)(123)(true)(false)(true)(123)(false) const curried2Return2 curried2(123, 123)(true, false)(true, 123)(false) const curried2Return3 curried2(123, 123)(true)(false)(true, 123, false) const curried2Return4 curried2(123, 123, true)(false, true, 123)(false) const curried2Return5 curried2(123, 123, true)(false)(true)(123)(false) const curried2Return6 curried2(123, 123, true, false)(true, 123, false) const curried2Return7 curried2(123, 123, true, false, true)(123, false) const curried2Return8 curried2(123, 123, true, false, true)(123)(false) const curried2Return9 curried2(123, 123, true, false, true, 123)(false) const curried2Return10 curried2(123, 123, true, false, true, 123, false)这十种写法覆盖了「每次 1 个」「每次多个」「混合分批」等几乎所有合法的分拆组合且curried2Return1 ~ curried2Return10的类型都必须精确等于boolean。这意味着你的类型实现必须支持任意前缀长度的分批调用而不是只能每次递归一个参数。两个必须报错的用例// ts-expect-error const curried1ReturnWrong curried1(123)(123)(wrong arg type) // ts-expect-error const curried1ReturnWrong2 curried1(123)()(123)(true)curried1ReturnWrong第三个参数传成了string而原函数第三个参数是boolean必须类型报错curried1ReturnWrong2中间出现了一次空调用()零参数违反「每次调用至少一个参数」的规则必须类型报错。ts-expect-error要求这两行的确产生类型错误否则测试本身会失败。断言机制所有返回值的类型通过Equal严格断言type cases [ ExpectEqualtypeof curried1Return1, boolean, // ... 其余 12 个断言 ]Equal与Expect定义在仓库的 utils/index.d.ts 中EqualX, Y使用「同一泛型函数签名的条件类型比较」实现结构等价判断ExpectT extends true T则要求断言结果严格为true。测试文件从type-challenges/utils导入它们而根目录 package.json 中的type-challenges/utils: workspace:*说明该包以 pnpm workspace 形式链接到仓库的 utils 工作区。从 Currying 1 到 Currying 2解法推导回顾 Currying 1每次恰好一个参数00017 的测试用例 展示了 Currying 1 的期望结果const curried1 Currying((a: string, b: number, c: boolean) true) // 期望类型 // (a: string) (b: number) (c: boolean) true const curried3 Currying(() true) // 期望类型() trueCurrying 1 的典型解法是「单参数递归」——每次从元组头部取出一个参数类型生成一层函数declare function CurryingT extends unknown[], R( fn: (...args: T) R, ): T extends [] ? () R : Curried1T, R type Curried1T extends unknown[], R T extends [infer F, ...infer Rest] ? (arg: F) Curried1Rest, R : R这种写法只能让柯里化后的函数每次恰好接收一个参数无法满足 Currying 2 中curried1(123, 123)(false)这种「一批传多个参数」的调用方式。Currying 2 的难点Currying 2 需要让每次调用自由选择参数个数至少一个。这就要求类型实现能够回答两个问题本次调用传入的参数P是不是原参数列表T的一个合法前缀类型与顺序都要匹配如果是把前缀消耗掉剩余部分Rest继续递归柯里化如果不是返回never使调用报错。「元组前缀匹配」正是通过变长元组推断实现的T extends [...P, ...infer Rest]。当P是T的前缀时TypeScript 可以把Rest推断为剩余参数类型同时还要处理「P恰好等于T」的终止情况以及「P为空」的非法情况。参考实现下面给出一种能够完整通过 test-cases.ts 全部断言的参考实现仓库 template.ts 仅给出declare function DynamicParamsCurrying(fn: any): any的起点解答正是从这条声明出发declare function DynamicParamsCurryingT extends unknown[], R( fn: (...args: T) R, ): T extends [] ? () R : CurriedT, R type CurriedT extends unknown[], R P extends unknown[]( ...args: P ) P extends [] ? never // 规则 2每次调用至少一个参数 : P extends T // 规则 3参数已全部给齐 ? R // → 返回原函数返回值 : T extends [...P, ...infer Rest] // P 是 T 的合法前缀 ? CurriedRest, R // → 消耗 P继续柯里化剩余参数 : never // 数量/类型不匹配 → 调用报错逐层拆解外层DynamicParamsCurryingT, R先从原函数推断出参数元组T与返回值R。若T为空元组零参数函数直接返回() R——这对应题目「函数参数个数可为零」的规则也与 Currying 1 中Currying(() true)得到() true的行为一致。P extends unknown[](...args: P)柯里化后的每一层都是一个泛型函数调用方本次传入的参数被推断为元组P从而支持一次传入任意多个参数。P extends [] ? never显式拦截空调用。没有这个分支空调用会落入前缀匹配空元组天然是任意元组的前缀导致无休止递归或错误类型。P extends T ? R本次调用恰好覆盖全部剩余参数直接返回原函数返回值类型R。T extends [...P, ...infer Rest]P是T的合法前缀长度不超过、逐个类型匹配把剩余部分Rest交给下一层递归。never其余一切情况类型不匹配、参数过多等都归于never使该调用在类型层面报错。用手动推演验证以curried1(123, 123)(false)为例此时T [string, number, boolean]R true第一层调用curried1(123, 123)P [string, number]。P extends []为假[string, number] extends [string, number, boolean]为假走前缀匹配[string, number, boolean] extends [string, number, ...infer Rest]得到Rest [boolean]返回Curried[boolean], true。第二层调用(false)P [boolean]。P extends []为假[boolean] extends [boolean]为真 → 返回true。再看必须报错的curried1(123)()(123)(true)第一层(123)P [string]前缀匹配得Rest [number, boolean]返回Curried[number, boolean], true。第二层()P []命中P extends [] ? never→ 类型为never调用报错满足ts-expect-error。7 参数函数的任意分批模式同理无论每次传 1 个还是多个只要逐层做前缀匹配并递归消耗最终都能在最后一层通过P extends T回到boolean。边界情况与易错点空调用必须报错这是与 Currying 1 最容易被忽略的差异。若不显式处理P extends []空调用会被当作「空前缀」继续递归产生错误类型甚至触发「类型实例化过深」的编译错误而不是干脆利落的类型错误。参数类型必须逐位匹配前缀匹配T extends [...P, ...infer Rest]会同时校验元素类型。curried1(123)(123)(wrong arg type)正是因为string不匹配boolean而落入never。一次性给满参数curried1(123, 123, true)中P恰好等于T命中P extends T ? R分支直接得到boolean不需要再产生一层函数。参数过多若某次调用传入的参数数量超过剩余数量T extends [...P, ...infer Rest]无法匹配结果为never同样报错。unknown[]而非any[]使用unknown[]作为泛型约束可以保留参数的类型信息避免any污染推断结果这是类型挑战中「宁可严格、不可放水」的通用原则。递归深度测试中最长的参数列表是 7 个逐层递归的深度为 7远低于 TypeScript 默认的递归上限无需担心「类型实例化过深」。在本仓库中动手验证本仓库是可读的挑战合集你可以这样复现与验证在 template.ts 中把declare function DynamicParamsCurrying(fn: any): any替换为上述参考实现注意template.ts无 import/export属于全局脚本文件其中的declare声明会被同编译单元内的测试文件感知。用 TypeScript 编译器对 test-cases.ts 做类型检查仓库 devDependencies 中的 typescript 版本为^5.3.3见根目录 package.json变长元组推断所需的最低版本为 TS 4.0。例如npx tsc --noEmit questions/00462-extreme-currying-2/template.ts questions/00462-extreme-currying-2/test-cases.ts具体编译选项以仓库的 tsconfig.json 与 tsconfig.base.json 为准。若类型检查零错误说明 13 条ExpectEqual...断言全部通过、两处ts-expect-error也确实报错解答即成立。可参考的对照材料还包括 Currying 1 的测试用例体会单参数递归与多参数前缀递归的差异以及 utils/index.d.ts 中Equal的实现。小结Currying 2 是 type-challenges 中「Extreme / 地狱级」难度的代表性题目它要求把「任意分批、每次至少一个参数、给满即返回原返回值」的运行时语义完整编码进类型系统。核心套路「T extends [...P, ...infer Rest]前缀匹配 递归柯里化 空调用拦截」不仅适用于本题也广泛复用于其余涉及「元组分段消费」的挑战例如 Split、Chunk、DropString 等。掌握它你就掌握了类型级「流式参数分发」的通用解法。赞分享示例工程【免费下载链接】type-challengesCollection of TypeScript type challenges with online judge项目地址https://gitcode.com/GitHub_Trending/ty/type-challenges点击查看免费下载相关推荐TypeScript 类型挑战type-challenges 00462 柯里化 2Extreme—— 用类型系统实现动态参数柯里化TypeScript 类型挑战type challenges 00462 柯里化 2Extreme—— 用类型系统实现动态参数柯里化 本篇指南围绕 que示例工程type-challenges 题解Hard 难度 Currying 1柯里化 1类型体操完整解析type challenges 题解Hard 难度 Currying 1柯里化 1类型体操完整解析 导读 Currying 1编号 17是 type示例工程TypeScript 类型挑战 00017用类型系统实现柯里化Currying的完整实战指南TypeScript 类型挑战 00017用类型系统实现柯里化Currying的完整实战指南 本篇技术指南围绕 type challenges 仓库中编号示例工程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表