Reputation: 15760
I have a base model class that looks like this:
class Base {
abstract public save(): Promise<Base>;
}
My models inherit from Base and must implement the save
method. For example,
class User extends Base {
public save(): Promise<User> {
// save my user in the database and return a promise like this...
return getDatabaseConnection()
.then((db) => db.insert(...))
.then((res) => this);
}
}
This works fine and Typescript has no problems with the types here.
But as I develop, I'm noticing that my model save
methods are all very similar...
class Group extends Base {
public save(): Promise<Group> {
// save my group in the database and return a promise like this...
return getDatabaseConnection()
.then((db) => db.insert(...))
.then((res) => this);
}
}
class OtherModel extends Base {
public save(): Promise<OtherModel> {
// save my other model in the database and return a promise like this...
return getDatabaseConnection()
.then((db) => db.insert(...))
.then((res) => this);
}
}
So it occurred to me that, in the interest of keeping things DRY, I could implement a decorator to add the save
method to the classes.
const save = (table: string, idColumn: string) => {
return function decorator<T extends {new(...args: any[]): {}}>(target: T): T {
return class extends target {
public save = function() {
// save whatever model this is & return it
return getDatabaseConnection()
.then((db) => db.insert(...use the table, idColumn args...))
.then((res) => this);
};
}
};
};
@save('user_table', 'id')
class User extends Base {}
@save('group_table', 'id')
class Group extends Base {}
@save('other_table', 'id')
class OtherModel extends Base {}
It works like a charm, except... Typescript complains that the abstract method save
is missing from my class declarations.
I have been working around it with @ts-ignore
statements, but I'd like to remove them.
I came across this question which is about new methods added to a class via decorators, and I understand that the decorator is not intended to modify the interface or contract, but that's not what I'm doing here. I'm trying to implement an abstract method that exists (and must be implemented) per the interface.
How can I get Typescript to recognize that I actually did implement abstract methods when I do it via a decorator?
Upvotes: 2
Views: 197
Reputation: 328262
I think the answer to your question as asked is that you should use a definite assignment assertion for the save()
method on all your subclasses, which tells the compiler that the method has been implemented even though it can't verify this. Like this:
@save('user_table', 'id')
class User extends Base {
save!: () => Promise<this>;
}
@save('group_table', 'id')
class Group extends Base {
save!: () => Promise<this>;
}
@save('other_table', 'id')
class OtherModel extends Base {
save!: () => Promise<this>;
}
That will suppress the errors, although it's a bit repetitive and requires more manual annotation than I expect you'd like.
A different approach here might be to use a class factory instead of a decorator. You are allowed to make a class extend any expression that evaluates to a class constructor. This is not very different from what you're doing with the decorator. First we make the factory:
const Save = (table: string, idColumn: string) => class extends Base {
public save() {
// save whatever model this is & return it
return Promise.resolve(this);
}
};
The implementation of the save()
method above can be anything you want as long as it is appropriate for Base
. The difference from the decorator is that the call to Save
returns a class directly which extends Base
, instead of returning a function that extends a class constructor passed into it. And then your subclasses become:
class User extends Save('user_table', 'id') {
}
class Group extends Save('group_table', 'id') {
}
class OtherModel extends Save('other_table', 'id') {
}
These are automatically seen as proper subclasses of Base
and so there's no error anywhere to worry about.
Okay, hope that helps; good luck!
Upvotes: 3