I'm trying to share some model objects between the client and server code (all in TypeScript). I would like to create the "shared" model so that it doesn't know anything about where it's being used, and then provide a mixin to add the server functionality (e.g., fetch from a database) on the server side, and a different mixin to provide the client functionality (e.g., fetch from a RESTful API) on the client.
Here's a simplified version of what I have so far (here's a playground link, too):
// generic declaration of a constructor type to make things easier later on
type Constructor<T> = new (...args: any[]) => T;
// this is the base model class which can be used by client or server code
class Model extends Object {
public id: number = 1;
}
// this is a specific model subclass, also usable on client or server
class Widget extends Model {
public length: number = 10;
}
// this class is only used on the server, but needs to know the type of model being acted upon
class ServerHelper<T> {
public async insert(model: T): Promise<T> { /* do the insert */ return Promise.resolve(model); }
}
// this is the public interface for the server-side mixin
interface ServerModel<M extends Model> {
helper: ServerHelper<M>;
}
// this is the server-side mixin which
function ServerModel<B extends Constructor<Model>>(Base: B): B & Constructor<ServerModel<InstanceType<B>>> {
type M = InstanceType<B>;
const result = class BaseWithServerModel extends Base {
public helper: ServerHelper<M> = new ServerHelper<M>();
public async insert(): Promise<this> {
return await this.helper.insert(this);
}
};
return result;
}
class SpecialWidget extends ServerModel(Widget) {
// this class needs this.helper to be a `Helper<Widget>`
}
I've been trying to adapt the constrained mixin example, but I can't find any formulation which gives me access to the type being mixed into (i.e., Widget
in my example) so that it can be passed along to other generic types.
Instead, I get a bunch of error like this one on the return result;
line:
'BaseWithServerModel' is assignable to the constraint of type 'M', but 'M' could be instantiated with a different subtype of constraint 'Model'.
I've spent hours digging around on the web and tinkering with various incantations on my own, but I've got nothing. Any suggestions on how I should declare my mixin so that I get access to M
?
The compiler cannot reason very well about the behavior of types that depend on an unspecified generic like
B
inside the body ofServerModel
. Especially when the type is a conditional type like theInstanceType<T>
utility type. So unfortunately code like the following just results in an error:The knows that
new ctor()
is anobject
, but it doesn't realize that it must be anInstanceType<T>
. See microsoft/TypeScript#37705 for a related issue.If you want this to compile you will need to use something like a type assertion to tell the compiler that you know it's correct even though it doesn't:
So you'll need something like this in your code:
A similar limitation is present with the polymorphic
this
type. Inside the body of a class, thethis
type is essentially a generic type parameter constrained to the type of the current class. So the compiler will not always be sure whether some value is assignable tothis
.In your case,
the compiler is technically correct to complain. The return type of
insert()
should be aPromise<this>
wherethis
is the type of whatever subclass ofBaseWithServerModel
is being used. Butthis.helper.insert()
returns onlyPromise<M>
. And is is possible thatthis
will be a proper subclass ofM
.If you don't care about such a possibility because it's unlikely, you can use another type assertion:
This compiles with no errors and lets you move on with your life. Maybe there are some better and more type safe solutions, but it might not be worth it.
In your comment you mentioned
which may work as well.
Playground link to code