Reputation: 845
Trying to create a type safe curried function. All the answers I'v seen on SO suggest function overloading, which I'd prefer to not do.
I came across type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
It does give all the function's argument types and its possible to select an argument type by its index. However, how do I slice ArgumentTypes to only have correct number of arguments and their types?
In the example below curried(1, 1);
give the error "Expected 3 arguments, but got 2".
function curry<T extends Function>(fn: T) {
const fnArgs: readonly string[] = args(fn);
const cachedArgs: readonly any[] = [];
type ArgumentTypes<F extends Function> = F extends (...args: infer A) => any ? A : never;
type FnArguments = ArgumentTypes<typeof fn>;
function curryReducer(...args: FnArguments) {
cachedArgs.push(...args);
return cachedArgs.length >= fnArgs.length
? fn(...cachedArgs)
: curryReducer;
}
return curryReducer;
}
function args<T extends Function>(fn: T): readonly string[] {
const match = fn
.toString()
.replace(/[\r\n\s]+/g, ' ')
.match(/(?:function\s*\w*)?\s*(?:\((.*?)\)|([^\s]+))/);
return match
? match
.slice(1, 3)
.join('')
.split(/\s*,\s*/)
: [];
}
function adding(a: number, b: number, c: number) {
return a + b + c;
}
const curried = curry(adding);
const add2 = curried(1, 1);
```;
Upvotes: 2
Views: 1434
Reputation: 4010
I wouldn't advise using regex to manipulate the function in string format - instead you can create a pretty simple curry function with the following format (in javascript):
function curry (fn) {
return (...args) => {
if (args.length >= fn.length) {
return fn(...args);
}
return (...more) => curry(fn)(...args, ...more);
}
}
This function takes the function fn and then returns a new function which takes some amount of arguments (...args).
We can then check if args is as long as the required number of arguments for the function (which is just function.length). This is the base case, which is like so:
const add = (a, b, c) => a + b + c;
add.length // = 3
curry(add)(1, 2, 3) // = 6
In the case where we apply less than the required arguments, we need to return a new function. This function will take some more arguments (...more) and append these to the original ...args, i.e:
(...more) => curry(fn)(...args, ...more);
To make this play with typescript, this gets a bit complicated. I would suggest taking a look at this tutorial to understand better.
I've adapted their CurryV5 variant (as placeholders are out of scope) and updated to use more modern typescript features, as the syntax is simpler. This should be the minimum to get the behaviour you're after:
// Drop N entries from array T
type Drop<N extends number, T extends any[], I extends any[] = []> =
Length<I> extends N
? T
: Drop<N, Tail<T>, Prepend<Head<T>, I>>;
// Add element E to array A (i.e Prepend<0, [1, 2]> = [0, 1, 2])
type Prepend<E, A extends any[]> = [E, ...A];
// Get the tail of the array, i.e Tail<[0, 1, 2]> = [1, 2]
type Tail<A extends any[]> = A extends [any] ? [] : A extends [any, ...infer T] ? T : never;
// Get the head of the array, i.e Head<[0, 1, 2]> = 0
type Head<A extends any[]> = A extends [infer H] ? H : A extends [infer H, ...any] ? H : never;
// Get the length of an array
type Length<T extends any[]> = T['length'];
// Use type X if X is assignable to Y, otherwise Y
type Cast<X, Y> = X extends Y ? X : Y;
// Curry a function
type Curry<P extends any[], R> =
<T extends any[]>(...args: Cast<T, Partial<P>>) =>
Drop<Length<T>, P> extends [any, ...any[]]
? Curry<Cast<Drop<Length<T>, P>, any[]>, R>
: R;
function curry<P extends any[], R>(fn: (...args: P) => R) {
return ((...args: any[]) => {
if (args.length >= fn.length) {
return (fn as Function)(...args) as R;
}
return ((...more: any[]) => (curry(fn) as Function)(...args, ...more));
}) as unknown as Curry<P, R>;
}
const add = curry((a: number, b: number, c: number) => a + b + c);
const add2 = add(1, 1);
add2(3); // 5
add2(3, 4); // error - expected 0-1 arguments but got 2
add2('foo'); // error - expected parameter of type number
Update - June 22 As noted by Ricardo below, this approach doesn't work for currying generic functions as TypeScript infers the generics as unknowns - because TS can't keep the generics unresolved, as they should be. This github comment explains this problem in a bit more detail.
The following case demonstrates this issue:
declare function addT<T>(a: T, b: number): T;
const addTCurried = curry(addT);
const result = addTCurried(0)(2); // type -> unknown (should be number)
Off the top of my head, this seems typesafe as you'd be forced determine result is of type number in order to use it as a number, but it's not great ergonomics - but I might be wrong here.
So, if you want to curry generic functions, then do it manually. If you're not doing that, then this approach should be ok.
If typescript supports higher-kindled types in future, then a typesafe curry of generic functions may become possible.
Upvotes: 4