Reputation: 5844
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
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!
Upvotes: 1