0
votes

I would like to extend a class and make it's dynamic type collapse into specific type (from T to Person).

I have created dummy classes for this example:

class Person {
    constructor(public name: string){}
}

class Base<T> {
    reference?: T
    name: string

    constructor(name: string) {
        //  Assign properties from T to this
        this.name = name
    }

    static fromObject<T extends { name: string }>(object: T){
        const base = new Base(object.name)
        base.reference = object
        return base
    }
}

class Master<T> extends Base<T> {

    static fromBase<T>(base: Base<T>){
        const master = new Master(base.name)
        master.reference = base.reference
        return master
    }

    static fromObject<T extends { name: string }>(object: T){
        return Master.fromBase(Base.fromObject(object))
    }
}

class PersonMaster extends Master<Person>  {

    constructor(person: Person){
        super(person.name)
        this.reference = person
    }

    static fromBase(base: Base<Person>){
        return new PersonMaster(base)
    }
}

The compiler returns this error:

Class static side typeof PersonMaster incorrectly extends base class static side typeof Master. Types of property fromBase are incompatible. Type (base: Base<Person>) => PersonMaster is not assignable to type <T>(base: Base<T>) => Master<{}>. Types of parameters base and base are incompatible. Type Base<T> is not assignable to Base<Person>. Type T is not assignable to type Person.

1

1 Answers

1
votes

The declaration of fromBase in Master is generic:

static fromBase<T>(base: Base<T>)

This is universal quantification: it says that fromBase should work for all T. So when you try to specialise the argument to Base<Person> in PersonMaster, the type checker rightly complains that this implementation of fromBase doesn't work for all Ts, just for Person. The type parameters of an overriding method have to match the type parameters of the declared method.

To put it another way, the T declared in fromBase is a totally different T than the one from the enclosing scope. It just happens to shadow the name. It might be easier to understand if the two type parameters had different names:

class Master<T> extends Base<T> {
    static fromBase<U>(base: Base<U>) { /* ... */ }
}

In this instance, I suspect you intended to use the (rigid) T from Master, not a fresh (higher-rank) T. Sadly, as you point out in your comment, you can't use a static method to do this because TypeScript doesn't support it. (I haven't been able to find any documentation as to why the language designers made this decision. I personally can't think of any good reasons, though I'm sure there is one.) So you have to move your method to an instance method on some other object:

interface MasterFactory<T> {
    fromBase(base: Base<T>): Master<T>
}
class PersonMasterFactory implements MasterFactory<Person> {
    fromBase(base: Base<Person>): Master<Person> { /* ... */ }
}
let personMasterFactory = new PersonMasterFactory();

With this design, calls which would have gone to PersonMaster.fromBase will go to personMasterFactory.fromBase.

Overriding a static method is a pretty strange thing to do in the first place. Method overriding is about dynamically dispatching calls based on the type of the receiver, but (conceptually, at least) a static method has no receiver. The clue's in the name: it doesn't make sense to dynamically dispatch a static method!