拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

TypeScript 类型挑战 00017:用类型系统实现柯里化(Currying)的完整实战指南

TypeScript 类型挑战 00017:用类型系统实现柯里化(Currying)的完整实战指南
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

本篇技术指南围绕 type-challenges 仓库中编号 00017 的困难(hard)级挑战「柯里化 1」展开,讲解如何用 TypeScript 类型系统为任意参数个数的函数推导出柯里化后的类型签名。读完本文,你将掌握变长元组(variadic tuple)与递归条件类型在函数类型变换中的组合用法,能够独立完成本挑战,并为理解仓库中更高难度的「柯里化 2」动态参数柯里化挑战打下基础。

挑战背景:柯里化是什么

柯里化(Currying) 是一种将带有多个参数的函数转换为一系列每个只带一个参数的函数的技术。例如一个普通的加法函数:

const add = (a: number, b: number) => a + b const three = add(1, 2)

经过柯里化之后,它的调用形态会从add(1, 2)变成逐次传参:

const curriedAdd = Currying(add) const five = curriedAdd(2)(3)

本挑战(编号 00017)的难点在于:传递给Currying的函数参数个数是不确定的,你需要在类型层面写出一个通用的变换,使得无论传入几个参数、什么类型,都能得到对应的逐层柯里化函数类型。

本挑战标注了「困难(hard)」难度,并归入#array(元组/数组)主题分类;文档同时建议在TypeScript 4.0下完成,这是因为本解法依赖 TS 4.0 引入的「变长元组类型(variadic tuple types)」能力([...infer Rest]语法)。

挑战目标与类型规格

原文档给出了本挑战的完整规格,需要满足以下三点:

  1. 柯里化后的函数每次仅接受一个参数;
  2. 参数逐个被分配,接受完所有参数后,返回最终结果;
  3. 柯里化函数每一层的返回值类型必须精确推导,不能退化成any。

也就是说,对于(a: string, b: number, c: boolean) => true这样的三参数函数,期望的类型是:

(a: string) => (b: number) => (c: boolean) => true

题目模板与测试用例解读

模板文件

本挑战的起点在 questions/00017-hard-currying-1/template.ts,模板只有一个空实现:

declare function Currying(fn: any): any

所有参数和返回值都被any吞掉了,任务就是把它替换成能正确推导柯里化类型的签名。

测试用例

仓库为每个挑战都提供了类型级测试用例,见 questions/00017-hard-currying-1/test-cases.ts。它通过Equal工具类型(定义于 utils/index.d.ts)逐一比对推导结果:

import type { Equal, Expect } from '@type-challenges/utils' const curried1 = Currying((a: string, b: number, c: boolean) => true) const curried2 = Currying((a: string, b: number, c: boolean, d: boolean, e: boolean, f: string, g: boolean) => true) const curried3 = Currying(() => true) type cases = [ Expect<Equal< typeof curried1, (a: string) => (b: number) => (c: boolean) => true >>, Expect<Equal< typeof curried2, (a: string) => (b: number) => (c: boolean) => (d: boolean) => (e: boolean) => (f: string) => (g: boolean) => true >>, Expect<Equal<typeof curried3, () => true>>, ]

从用例中可以提炼出本挑战必须覆盖的三个边界:

用例输入期望输出覆盖要点
curried13 个参数(string, number, boolean)3 层柯里化链基本多参数场景
curried27 个参数7 层柯里化链长参数列表与类型逐层对应
curried30 个参数() => true() => true(原样返回)空参数边界

注意curried3说明了一个重要细节:当函数没有参数时,柯里化结果就是它本身,不能推导成错误的形态。

解题思路:四步构造柯里化类型

第一步:约束入参并提取参数与返回值

首先把Currying的入参约束为函数类型,并用infer拆出参数元组Args与返回值R:

declare function Currying<T extends (...args: any[]) => any>(fn: T): Curried<T> type Curried<F> = F extends (...args: infer Args) => infer R ? /* 递归展开 Args */ : never

infer Args得到的不是一个函数签名,而是一个元组,例如[string, number, boolean],这正是后续递归的原料。

第二步:处理空参数边界

当Args是空元组[]时,函数没有参数可分,直接返回一个接受零参数并返回R的函数类型:

type Curried<F> = F extends (...args: infer Args) => infer R ? Args extends [] ? () => R : /* 继续递归 */ : never

这一步对应测试用例中的curried3。

第三步:逐个拆出参数并递归生成下一层

当Args非空时,用变长元组语法拆出头元素A与剩余元组Rest:

Args extends [infer A, ...infer Rest] ? (arg: A) => Curried<(...args: Rest) => R> : never

这里的关键是:拆出第一个参数A作为当前层函数的入参,然后把剩余参数 + 原返回值重新包成一个函数(...args: Rest) => R,继续交给Curried递归处理。每次递归吞掉一个参数,直到Rest为空触发第二步的边界分支。

第四步:完整的参考解法

将以上三步组合,得到一份可提交的解法:

declare function Currying<T extends (...args: any[]) => any>(fn: T): Curried<T> type Curried<F> = F extends (...args: infer Args) => infer R ? Args extends [] ? () => R : Args extends [infer A, ...infer Rest] ? (arg: A) => Curried<(...args: Rest) => R> : never : never

用测试用例验证推导过程:

  • 对(a: string, b: number, c: boolean) => true:第一轮拆出string得到(arg: string) => Curried<(b: number, c: boolean) => true>,第二轮拆出number,第三轮拆出boolean后Rest为空,落入() => R分支返回true,最终展开为(a: string) => (b: number) => (c: boolean) => true,与curried1的期望完全一致;
  • 对curried2的 7 参数函数,递归会连续展开 7 层,类型推导逐一对应;
  • 对curried3的空参数函数,Args extends []直接命中,返回() => true。

知识点剖析:这道题练的是什么

变长元组类型(Variadic Tuple Types)

[infer A, ...infer Rest]是 TS 4.0 的核心新语法,它允许在元组中间/末尾展开剩余元素。没有它,我们只能写[infer A, ...]这类无法捕获剩余部分的模式。这正是原文档建议「在 TypeScript 4.0 下完成」的原因——若使用更早版本,...infer Rest无法编译通过。

条件类型 + infer 的递归

Curried是典型的递归条件类型:它以F extends (...args: infer Args) => infer R为入口,在真分支内继续构造新的函数类型并调用自身。递归的终止条件是Args extends [],与运行时柯里化的「参数耗尽即返回结果」语义一一对应。

与仓库相关挑战的关联

仓库文档在本挑战底部列出了三条相关挑战,可以作为练习链路:

  • 14・第一个元素:用infer A从元组中提取首元素,是本挑战[infer A, ...infer Rest]拆解的前置热身;
  • 16・排除最后一项:对元组做「去尾」操作,练习元组的拆解与重组;
  • 462・柯里化 2:地狱级进阶版,要求柯里化函数每次可接受至少一个参数、数量不限(对应bind这类动态传参场景),在理解本挑战「每次一个参数」的递归骨架后,可进一步挑战该题。

验证与运行方式

本仓库是类型挑战集合,所有用例通过Equal进行编译期断言。完成template.ts的填写后,可在本地运行 TypeScript 编译器(仓库根目录 package.json 的 devDependencies 中配置了typescript ^5.3.3),以tsc检查 questions/00017-hard-currying-1/test-cases.ts 是否存在类型错误:

npx tsc --noEmit questions/00017-hard-currying-1/test-cases.ts

若Equal断言通过,说明你的Curried类型推导与期望完全一致(Equal的实现基于函数参数逆变比较,比简单的extends互检更严格,见 utils/index.d.ts)。

小结

本挑战的核心价值在于训练「把运行时行为映射为递归类型变换」的思维:柯里化把f(a, b, c)变成f(a)(b)(c),类型层面就对应把参数元组[A, B, C]递归展开为(arg: A) => (arg: B) => (arg: C) => R。掌握了「infer 拆元组 + 递归终止分支 + 变长元组重组」这套组合拳,你就拥有了应对绝大多数函数签名变换类挑战(包括后续的柯里化 2)的核心能力。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:开源项目Anchor CMS快速指南与常见问题解答
下一篇:Webgrind 开源项目常见问题解决方案

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

返回列表