I don't see any way to use a workaround that specifies generic functions to help you here. That workaround takes a generic call signature and turns it into a generic type... but you need to turn one generic call signature into another generic call signature, which this doesn't do. And in any case, the workaround only works for individual generic call signatures, not for any possible one passed to curried()
. So I will stop considering this.
There is some support for inferring generic functions but it only works in very specific circumstances and is thus fragile. Specifically it does not work when producing overloaded functions, such as you are doing with the result of curried()
. See microsoft/TypeScript#33594 for more information. If you need overloads to come out of curried()
, then it is not currently possible.
If we relax the requirement for overloads and only produce the output that peels one parameter off the end of the function, you can write the call signature for curried()
like this:
declare function curried<I extends any[], L, R>(fn: (...args: [...I, L]) => R):
((...args: I) => (last: L) => R)
You can verify it works like this:
function z<T>(a: T, b: T, c: T) { return [a, b, c]; }
const cZ = curried(z);
// const cZ: <T>(a: T, b: T) => (last: T) => T[]
const cz2 = cZ(5, 6);
// const cz2: (last: number) => number[]
Generic in, generic out. The z
function is generic with type parameter T
, and so cZ
is generic as well, and can be called to specify T
. Hooray!
Note that I didn't use your a
function to demonstrate. It still "works", but it's not wonderful. Let's try it:
function a<T>(b: string, c: number, d: T[]) { return [b, c, ...d]; }
const f = curried(a);
// const f: <T>(b: string, c: number) => (last: T[]) => (string | number | T)[]
const f2 = f('hi', 42);
// const f2: (last: unknown[]) => unknown[]
The output f
is generic, all right. But there's no good inference site for the type parameter T
. When you call f('hi', 42)
, the compiler needs to infer T
right then, but what could T
be? It doesn't depend on 'hi'
or 42
. The compiler can't possibly know, so it falls back to unknown
anyway.
The only way around that is to specify T
yourself when you call f
, which makes its generic nature a bit less impressive:
const f3 = f<number>('hi', 42)
// const f3: (last: number[]) => (string | number)[]
Arguably the "right" generic type for the output of curried(a)
should be
declare const f: (b: string, c: number) => <T>(last: T[]) => (string | number | T)[]
where I've moved T
from the outer function scope to the returned function scope. If you had that, then your call to f()
would produce another generic function:
const f2 = f('hi', 42);
// const f2: <T>(last: T[]) => (string | number | T)[]
And so you wouldn't have unknown
anymore:
const result = f2([1, 2, 3]);
// const result: (string | number)[]
But there's just no obvious way for the compiler to know it should sometimes move the generic type parameter scope from the output function to the output function's output function. And there really isn't any way for a developer to tell the compiler to do it, since TypeScript lacks the expressiveness to talk about operations on generic call signatures.
That would require either higher kinded types of the sort requested in microsoft/TypeScript#1213 or generic values of the sort requested in microsoft/TypeScript#17574... or maybe even existentially quantified generics of the sort requested in microsoft/TypeScript#14466. Or maybe all three of them. Or something else. Right now, though, it seems to be out of reach.
So there you go. If you relax your requirements and put up with some edge cases, you can get close to what you want. If you can't, then I guess we might have to wait until some future version of TypeScript comes along with support for more powerful manipulation of generic function types.
Playground link to code