3

I want to be able to use a class-based method in Typescript - i.e. a method defined on a base class which, when called on a subclass, can access the subclass from which it was called. This behaviour is possible in Python, but as far as I can tell there is no equivalent in Typescript. What would be the best way to replicate this in Typescript?

Example (in Python):

This example (written in Python) demonstrates something I may want to do with this.

# Base class
class Model:
    objects = []

    def __init__(self, objId):
        self.objId = objId

        Model.objects.append(self)

    @classmethod
    def getById(cls, objId):
        # Method can access subclass using "cls" parameter
        objects = [obj for obj in cls.objects if obj.objId == objId]
        if len(objects) > 0:
            return objects[0]
        else:
            print("error")

# Subclass
class Foo(Model):
    def __init__(self, objId, name):
        self.objId = objId
        self.name = name

        Foo.objects.append(self)

Foo(1, "foo")
Foo(3, "bar")

# Call method on subclass
foo = Foo.getById(1)
bar = Foo.getById(3)

print(foo.name)  # outputs "foo"
print(bar.name)  # outputs "bar"

Foo.getById(2)  # outputs "error"

Typescript (not working):

This example shows the rough equivalent in typescript, but it doesn't work because of the lack of class methods.

class Model {
    static objects: Model[]

    id: number

    constructor (id) {
        this.id = id

        Model.objects.push(this);
    }

    // Here "cls" should refer to the class on which this method is called
    static getById (id): cls {
        let item = cls.objects.find(obj => obj.id == id);
        if (item === undefined) {
            console.log("error");
        } else {
            return item;
        }
    }
}

class Foo extends Model {
    name: string

    constructor (id, name) {
        super(id);

        this.name = name

        Foo.objects.push(this);
    }
}


new Foo(1, "foo");
new Foo(3, "bar");

// Here cls === Foo
let foo = Foo.getById(1);
let bar = Foo.getById(3);

console.log(foo.name);
console.log(bar.name);

Foo.getById(2)

Clearly this is easy to do with a single class, but I can't figure out a way to be able to use a method like this for multiple classes, without re-declaring it on each one.

Side question:

Is there any way to have an "objects" static property on each subclass, each typed to their subclass, without manually re-declaring it.

class Model {
    static objects: Model[]

class Foo extends Model {
    static objects: Foo[]

class Bar extends Model {
    static objects: Bar[]

Essentially, I want this behaviour, but without having to separately declare the "objects" property on each subclass. Is there any way to do that?

1 Answer 1

5

The this context of a static method is the class on which it was called. You can override the this type for getById to state that getById can be called on any subclass of Model (i.e., any object with a construct signature that constructs a subtype of Model) and returns the instance type of that class. Here is example code, assuming that all objects of all subclasses are stored in the single Model.objects array:

class Model {
    static objects: Model[]

    id: number

    constructor (id) {
        this.id = id

        Model.objects.push(this);
    }

    static getById<M extends Model>(
        this: { new(...args: any[]): M }, id): M {
        let item = Model.objects.find(obj => obj.id == id);
        if (item === undefined) {
            console.log("error");
        } else {
            return <M>item;
        }
    }
}

Note that this is unsound because someone could call Foo.getById with an ID that corresponds to a Bar instead of a Foo.

If you want a separate objects array for each subclass, you'll need to manually initialize the array in each subclass (there is no way to automate this), change the Model constructor to push into the objects array of the current class (which you can reference as this.constructor after declaring it), and then change getById to use this.objects, after declaring that it must exist.

class Model {
    static objects: Model[]
    "constructor": {
        // This implementation is slightly unsound: the element type
        // is actually the instance type of the constructor.  At least
        // the interface provided is safe.
        objects: Model[]
    };

    id: number

    constructor (id) {
        this.id = id

        this.constructor.objects.push(this);
    }

    static getById<M extends Model>(
        this: { new(...args: any[]): M, objects: M[] }, id): M {
        let item = this.objects.find(obj => obj.id == id);
        if (item === undefined) {
            console.log("error");
        } else {
            return item;
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.