Type a curry helper that accepts any number of arguments per call. Partial over the parameter tuple describes the legal prefixes, and each call drops what it consumed.
Currying 1 peeled one parameter per call. This one lets the caller decide how many to hand over.
DynamicParamsCurrying(fn) returns a function that swallows one or more of fn's parameters, then returns another such function for whatever is left, until nothing is left and you get fn's return type. Every split of the parameter list has to typecheck, and only the splits that respect the original order and arity.
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)
const seven = curriedAdd(1, 2)(4)
const nine = curriedAdd(2)(3)(4)A seven-parameter function has 64 legal call chains. Writing them out is not an option, so the type has to describe "some non-empty prefix of what is left" and then subtract that prefix from the remainder.
Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument.
But in our daily life, currying dynamic arguments is also commonly used, for example, the Function.bind(this, [...params]) API.
const func = (a: number, b: number, c: number) => {
return a + b + c
}
const bindFunc = func(null, 1, 2)
const result = bindFunc(3) // result: 6Thus, based on Currying 1, we would need to have the dynamic argument version:
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)
const seven = curriedAdd(1, 2)(4)
const nine = curriedAdd(2)(3)(4)In this challenge, DynamicParamsCurrying may take a function with zero to multiple arguments, you need to correctly type it. The returned function may accept at least one argument. When all the arguments as satisfied, it should yield the return type of the original function correctly.
View on GitHub: https://tsch.js.org/462
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
declare function DynamicParamsCurrying(fn: any): any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
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,
)
const curried1Return1 = curried1('123')(123)(true)
const curried1Return2 = curried1('123', 123)(false)
const curried1Return3 = curried1('123', 12Unlock 150+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
type DropUsed<
Args extends unknown[],
Used extends unknown[],
> = Used extends [unknown, ...infer UsedRest]
? Args extends [unknown, ...infer ArgsRest]
? DropUsed<ArgsRest, UsedRest>
: []
: Args
type Curried<Args extends unknown[], R> = Args extends [
infer First,
...infer Rest,
]
? <Used extends [First, ...Partial<Rest>]>(
...args: Used
) => DropUsed<Args, Used> extends []
? R
: Curried<DropUsed<Args, Used>, R>
: R
declare function DynamicParamsCurrying<Args extends unknown[], R>(
fn: (...args: Args) => R,
): Curried<Args, R>Two ideas carry it: a constraint that spells out every legal prefix at once, and a generic call signature that reads back how many arguments actually arrived.
Split the remaining parameters into First and Rest. A call must supply First, may supply as much of Rest as it likes, and must not skip anything in between. Partial on a tuple turns each element optional in place:
// Args = [a: string, b: number, c: boolean]
// Rest = [b: number, c: boolean]
// Partial<Rest> = [b?: number, c?: boolean]So [First, ...Partial<Rest>] accepts [string], [string, number] and [string, number, boolean], and nothing else. Optional tuple elements cannot be left out of the middle, which is what keeps (b: number) from being passed where a: string belongs. It is also why the empty call in curried1('123')()(123)(true) is rejected: First is required, so a zero-argument call cannot satisfy the constraint.
The constraint alone is not enough, because the return type depends on the number of arguments, not just their validity. That is what Used is for. It is a type parameter of the returned function, not of Curried, so TypeScript infers it fresh at every call site from the actual argument list:
// curried2('123', 123) infers Used = [string, number]
// curried2('123', 123, true) infers Used = [string, number, boolean]The literals widen to string and number here rather than staying '123' and 123, because the constraint's corresponding elements are the unwidened parameter types. That widening is also why R comes out as boolean and not the literal true: constraining the input as fn: (...args: Args) => R gives the passed lambda a contextual call signature, and its literal return type widens during inference. In Currying 1 that widening had to be avoided; here the tests ask for boolean, so it is exactly what you want.
DropUsed<Args, Used> walks both tuples in lockstep and returns whatever is left of Args:
// DropUsed<[string, number, boolean], [string]> is [number, boolean]
// DropUsed<[string, number, boolean], [string, number]> is [boolean]
// DropUsed<[boolean], [boolean]> is []It recurses on Used and stops when Used runs dry, handing back the untouched tail of Args. The inner Args extends [unknown, ...infer ArgsRest] guard only fires if Used were longer than Args, which the constraint already prevents; it is there to satisfy the compiler that the recursion has a floor.
Then the return type branches on the leftovers: DropUsed<Args, Used> extends [] ? R : Curried<DropUsed<Args, Used>, R>. Because Used always contains at least First, Args shrinks by one element or more on every step, so the recursion is guaranteed to reach the empty tuple. There is no accumulator to thread through, since the consumed parameters are simply thrown away.
curried1('123', 123, true) returns boolean directly. All three parameters are consumed in one call, DropUsed yields [], and the conditional short-circuits to R instead of producing another function.curried2 splits seven parameters ten different ways in the tests, from (1)(1)(1)(1)(1)(1)(1) to a single seven-argument call. Each shape is handled by the same two lines; nothing enumerates the splits.curried1('123')(123)('wrong arg type') must error. After two calls the remaining tuple is [boolean], so First is boolean and a string fails the constraint.curried1('123')()(123)(true) must error on the empty call, for the reason above.Args extends [infer First, ...infer Rest] check to the bare R. The tests do not exercise it, but the branch keeps the type total.Once you see the parameter list as a tuple you can slice, "how many arguments did you pass me" stops being a question about functions and becomes a question about tuple lengths, which the type system is happy to answer.
This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges