Kryten
Kryten

Reputation: 15760

How can I get Typescript to recognize that I actually did implement abstract methods when I do it via a decorator?

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

Answers (1)

jcalz
jcalz

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!

Link to code

Upvotes: 3

Related Questions