After going through variadic types I wanted to make this, but I'm wondering how to use array of functions.
This is my first try:
function curry<T extends any[]>(fn: (...args: T) => any) {
return function(...args: T) {
return args.length >= fn.length
? fn(...args)
: curry(fn.bind(undefined, ...args));
}
}
But for fn.bind
I'm getting "The 'this' context of type '(...args: T) => any' is not assignable to method's 'this' of type '(this: undefined, ...args: any[]) => any'."
Any ideas?
You're not really using variadic tuple tpes in your code. The specific flavor of curry()
you're implementing is one that takes a partial parameter list and then possibly returns another function that takes a partial remainder of the list. That means the initial tuple of parameters T
might be split into many different pieces, and this is therefore not going to be automatically inferred from context the way that the partialCall()
function in the documentation can.
Instead, you need to explicitly break the T
tuples apart into possible subtuples. Let's represent the type of the desired output of curry()
that takes a function whose parameters are the tuple A
and whose return type is R
:
type Curried<A extends any[], R> =
<P extends Partial<A>>(...args: P) => P extends A ? R :
A extends [...SameLength<P>, ...infer S] ? S extends any[] ? Curried<S, R>
: never : never;
type SameLength<T extends any[]> = Extract<{ [K in keyof T]: any }, any[]>
Let me translate that into English. A Curried<A, R>
is a generic function, whose arguments must be of some tuple type P
that is constrained to Partial<A>
.
For tuples, Partial<A>
ends up meaning that you can leave out any suffix of the tuple (from some place to the end). So [1, 2, 3]
is assignable to Partial<[1,2,3,4,5,6,7]>
, but [1, 2, 4]
is not. There's a bit of a wrinkle with undefined
, since [1, undefined, 3]
is also assignable to Partial<[1,2,3,4,5,6,7]>
, but I'm going to ignore that and if it becomes important it can be worked around. Anyway, it means that the arguments to Curried<A, R>
must be some prefix (initial chunk) of the A
tuple.
The return type of Curried<A, R>
depends on the prefix P
passed in. If P
is the whole tuple A
, then the return type is just R
(that's what happens when you've finally given the function all its arguments). Otherwise, you split A
into the prefix P
and its suffix S
, and return a new curried function of type Curried<S, R>
.
Splitting A
into [...SameLength<P>, ...infer S]
uses variadic tuple types. Note that SameLength<P>
is just a tuple the same length as P
but with its element types as any
. This avoids a problem where P
is inferred to be very narrow (say A
is [number, number, number]
and then P
is [0, 0]
. You can't split [number, number, string]
into [0, 0, ...infer S]
because number
isn't assignable to 0
. But all we care about is length here, so we split [number, number, string]
into [any, any, ...infer S]
and that works, and infers S
to be string
).
Okay, use and implement it:
function curry<A extends any[], R>(fn: (...args: A) => R): Curried<A, R> {
return (...args: any[]): any =>
args.length >= fn.length ? fn(...args as any) : curry((fn as any).bind(undefined, ...args));
}
I used a whole lot of type assertions inside the implementation of curry()
, because the compiler is almost hopelessly lost at trying to verify that the returned function will be assignable to Curried<A, R>
. Instead of worrying about it, I just tell the compiler not to bother verifying safety, and take responsibility myself for making the implementation correct. (If it's wrong, I blame myself, not the compiler).
Okay, so there we go. Does it work?
const fn = (a: string, b: number, c: boolean) => (a.length <= b) === c ? "yep" : "nope";
const cFn = curry(fn);
const val1 = cFn("")(1)(true);
console.log(val1); // yep
const val2 = cFn("", 1, true);
console.log(val2); // yep
const val3 = cFn()()()()("", 1)()()(true); // yep
Looks good to me. Note that according to my definition if you call a Curried<A, R>
with no arguments you just get back a Curried<A, R>
. Here are some intentional errors so you can see that the compiler catches them:
// errors
cFn(1, 1, true); // error!
// ~ <-- not a string
cFn("", 1, true, false); // error!
// ~~~~~ <-- Expected 0-3 arguments, but got 4
cFn("")(1)(false)(true); // error!
//~~~~~~~~~~~~~~~ <-- This expression is not callable.
These look like the correct errors to me.