Is there a way to wrap a generic function inside a class and preserve the generics?
Here is my idea:
const func = <G extends string>(p: G) => p;
class Func<A extends any[], R> {
constructor(private readonly func: (...args: A) => R) {}
public call(...args: A): R {
return this.func(...args);
}
public catch(cb: (error: unknown) => void) {}
// more methods ...
}
Now if I use it the generics are obviously lost:
const use = new Func(func); // => Func<[p: any], any>
I have tried this, which preserves the functions type as a whole:
class Func<F extends (...args: A) => R, A extends any[], R> {
constructor(private readonly func: F) {}
public call(...args: A): R {
return this.func(...args);
}
public catch(cb: (error: unknown) => void) {}
}
But even then, if I try to use the call
method, generics (and even parameter types) are lost:
use.call('str'); // => any
To summarize: I need a way to wrap a generic function in a class and preserve its generics for the class methods. I am open for any creative workaround.
Playground: link
Unfortunately TypeScript doesn't have much support for the sort of higher order manipulation of generic function types you're doing. The support it does have, as implemented in microsoft/TypeScript#30215 is quite limited, and doesn't apply when you are returning an object with the intended generic function as a method. So we can't leverage that, especially in the form where the function parameters and return type have separate type parameters (since that would break the generics).
Seeing as all you're trying to do is say that new Func(f).call
is the same exact type as f
, we can work around it by making Func
generic in the type of f
, and declaring call
to be that type. You can't do that with method syntax (there's an open feature request at microsoft/TypeScript#22063 to allow annotating a function statement and, presumably, a method, so it's a missing feature). You can use declaration merging to declare it as if it were a property, and then manually add call
to Func.prototype
, with some workaround to allow you to access a TypeScript-private
property inside of it (see microsoft/TypeScript#19335):
// class is generic in type of `func`:
class Func<F extends (...args: any) => any> {
constructor(private readonly func: F) { }
public catch(cb: (error: unknown) => void) { }
}
// merge in `call` as same type
interface Func<F> {
call: F;
}
// implement `call`
Func.prototype.call = function (this: Func<any>, ...args: any) {
return this["func"](...args);
// ^^^^^^^^ brackets let you access private member
}
And now if we try it we see the desired behavior:
const func = <G extends string>(p: G) => p;
const use = new Func(func);
const s = use.call('str');
// ^? const s: 'str'
So it's possible to do what you asked for in the question... but the solution is fragile. Only because call
is exactly the same type as this.func
does it work; if you needed something even slightly different that would need the type to be transformed in some way (e.g., the function returns something like {prop: this.func(...args)}
, or accepts an extra or fewer argument compared to this.func
, etc) then you'd end up with broken generics again.