I am working on converting an old JS codebase to TypeScript. I have a rather advanced scenario and I can't figure out how to assign types correctly. I'd like to avoid resorting to the any type if possible.
To start, I have a base class that establishes a common context and then several dozen classes that extend this base class:
class Context {}
class ContextDependent {
protected readonly context:Context;
constructor(context:Context) {
this.context = context;
}
}
class MyClass extends ContextDependent {
private myParam:string;
constructor(context:Context, myParam:string) {
super(context);
this.myParam = myParam;
}
}
(For what it's worth, by convention all classes that extend this base class take Context as their first argument)
I need to expose some of these classes to a public-facing API, but the context argument is not accessible via the public API, so I expose them by binding the first constructor argument:
function bindClass(context, Class) {
// magic happens
// This is a very naive solution given as a simple example.
// The actual logic is much more complex (so as to preserve instanceof checks, etc)
return Class.bind({}, context);
}
function getPublicApi(context) {
return {
MyClass: bindClass(context, MyClass),
}
}
This system has worked extremely well, but I'm now trying to figure out how to convert this to TypeScript, if it's even possible.
I've tried a couple iterations and the closest thing I've come up with is:
function bindClass<T extends ContextDependent>(context:Context, Class:new (context:Context) => T):new () => T {
// magic happens
}
But of course this does not work. While the type declaration is accepted, the constraint rejects sub-classes of ContextDependent that take more than the context argument:
bindClass(context, MyClass);
/*
Error:
Argument of type 'typeof MyClass' is not assignable to parameter of type 'new (context: Context) => MyClass'.
Types of construct signatures are incompatible.
Type 'new (context: Context, myParam: string) => MyClass' is not assignable to type 'new (context: Context) => MyClass'.ts(2345)
*/
I understand why this isn't working, but I can't find a solution that does quite what I'm looking for. Is there some way to include the additional arguments dynamically and then include them, something like:
function bindClass<T extends ContextDependent>(context:Context, Class:new (context:Context, ...args:unknown[]) => T):new (...args:[unknown]) => T {
// magic happens
}
/*
Error:
Argument of type 'typeof MyClass' is not assignable to parameter of type 'new (context: Context, ...args: unknown[]) => MyClass'.
Types of construct signatures are incompatible.
Type 'new (context: Context, myParam: string) => MyClass' is not assignable to type 'new (context: Context, ...args: unknown[]) => MyClass'.
Types of parameters 'myParam' and 'args' are incompatible.
Type 'unknown' is not assignable to type 'string'.ts(2345)
*/
