So I have a function for currying other functions:
const curry = <TArg, TReturn>(fn: (...args: TArg[]) => TReturn) => {
const curried = (...args: TArg[]) =>
args.length < fn.length
? (...innerArgs: TArg[]) => curried(...args, ...innerArgs)
: fn(...args);
return curried;
};
const join = (a: number, b: number, c: number) => {
return `${a}_${b}_${c}`;
};
const curriedJoin = curry(join);
curriedJoin(1)(2, 3); // Should return '1_2_3', but gives an error
Typescript doesn't let me call it more than once cause the first call might've returned a string. How would you fix it?
The only way this could work is if
curry()returns a function whose output type depends significantly on the number of parameters in the input function and the number of arguments passed in. That means you will need it to be not only generic, but also use conditional types to express the difference. And you'll need the type of that function to be recursive, so we have to give it a name:So a
Curried<A, R>represents a curried version of a function whose parameter list isAand whose return type isR. You call it with someargsof a generic rest parameter typeAA, which must be assignable to a partial version ofA(using thePartial<T>utility type). Then we need to figure out the rest of the parameter list, which isinferred to beAR(Note that perhapsA extends [...AA, ...infer AR] ? ⋯would work, but if for some reasonAAis narrower than the initial part ofA, that would fail. So{[I in keyof AA]: any}just means "anything that's the same length asAA"). IfARis empty ([]) then the curried function returnsR. Otherwise it returnsCurried<AR, R>, so that the resulting function can be called also.The implementation could look like:
Note that I used the
anytype and a type assertion to convince the compiler that the implementation ofcurryis acceptable. The compiler can't really understand many higher-order generic type operations, or verify what function implementations might satisfy them.Let's test it out:
Looks good, that all behaves as desired.
But there's a caveat. Any code that depends on the
lengthproperty of a function cannot be perfectly represented in TypeScript's type system. TypeScript takes the position that functions can safely be called with more arguments than the number of parameters, at least when it comes to assignability of callbacks. See the documentation and the FAQ. So you can have a function whoselengthdoes not agree with the number of parameters TypeScript knows about. Meaning you can possibly end up in this situation:myFilter()takes a callback of the type accepted by an array of strings'filter()method. This callback will be called with three arguments; that's howfilter()works in JavaScript. But TypeScript lets you pass callbacks that take fewer arguments. So you can call this:There's no compiler error anywhere, but you get a runtime error. The body of
myFilterthinks the callback has a length of3, but it really has a length of1at runtime. Oops.This might not happen in your actual code, but you should be aware of it.
Playground link to code