dodov
dodov

Reputation: 5844

Determine class instance member type based on static member type in subclass

I have a base class called Parent:

class Parent {
    static elType = window.Element
    el: InstanceType<typeof Parent['elType']>

    constructor(input: Element) {
        let ctor = this.constructor as typeof Parent
        if (input instanceof ctor.elType) {
            this.el = input
        } else {
            throw new Error()
        }
    }
}

It allows instances to be created only if input is an instance of elType specified in the constructor. If the check passes, an instance member el is set to input.

Then, I want to create a subclass that allows only HTMLElement (which extends Element) inputs:

class Child extends Parent {
    static elType = window.HTMLElement
}

However, the instance member el is not correctly set to HTMLElement. It's still Element:

let foo = null as unknown as HTMLElement
let ch = new Child(foo)

// Property 'offsetLeft' does not exist on type 'Element'.
ch.el.offsetLeft

I think the problem lies in this:

el: InstanceType<typeof Parent['elType']>

I'm setting the type of el to the elType type of Parent, which is Element and is not affected by Child's static elType. My question is - how can I make that work? I need some trick like:

el: InstanceType<typeof {{ current class }}['elType']>

Check this in the playground.


I know I can solve it by explicitly declaring el in Child:

class Child extends Parent {
    static elType = window.HTMLElement
    el: HTMLElement
}

But I want to avoid that as it's redundant. el should always be the instance type of static elType.

Upvotes: 0

Views: 184

Answers (1)

jcalz
jcalz

Reputation: 329763

If you weren't using static properties, I'd suggest using polymorphic this types to represent the constraint that a subclass property will narrow in concert with some other property. Something like this:

class Parent {
    elType = window.Element
    el: InstanceType<this['elType']>
    constructor(input: Element) {
        if (input instanceof this.elType) {
            this.el = input as InstanceType<this['elType']>; // assert
        } else {
            throw new Error()
        }
    }
}
class Child extends Parent {
    elType = window.HTMLElement
}
let foo = null as unknown as HTMLElement
let ch = new Child(foo)
ch.el.offsetLeft; // okay

Here the type of el is declared as InstanceType<this['elType']>, which will always be related to the specific type of elType in each subclass. That makes assigning something to this.el a little tricky, since the compiler can't easily verify that such an assignment is safe for all subclasses. A type assertion is the most straightforward way around that.

Anyway you can see that this behaves almost exactly as you want, except that the elType property is an instance property and not static.


If you really want to see this statically, I'd probably end up moving away from straight inheritance and instead I'd use a factory function that creates classes for you. Like this:

const ParentMaker = <T extends Element>(elType: new () => T) => {
    return class Parent {
        static elType = elType;
        el: T;
        constructor(input: Element) {
            let ctor = this.constructor as typeof Parent
            if (input instanceof ctor.elType) {
                this.el = input
            } else {
                throw new Error()
            }
        }
    }
}

const Child = ParentMaker(window.HTMLElement);
let foo = null as unknown as HTMLElement
let ch = new Child(foo)
ch.el.offsetLeft

Here ParentMaker takes a constructor as an argument and returns a new class whose static and instance side have elType and el properties strongly typed the way you want. Of course there's no easy route to inheritance here, but maybe that's all you need: you could always do class Child extends ParentMaker(window.HTMLElement) { ... } to make Child have its own properties and methods. You'd only run into trouble if you needed sub-subclasses or for ch instanceof Parent to work.


Hopefully one of those gives you some ideas for how to proceed. Good luck!

Link to code

Upvotes: 1

Related Questions