Reputation: 467
Given
abstract class A {
constructor() {
this.initialize()
}
initialize<T extends {
[t in keyof this]?: boolean
}>(todos?: T) {
// Do something
}
}
class B extends A {
initialize() {
super.initialize({ // <-- it will throw error starting on this open bracket
test: false
})
}
test() {
return 'test'
}
}
Why is it the code above throwing error stating that
{ test: false }
is not assignable to {[t in keyof this]?: boolean}
? While it clearly is.
'test' is one of the keys of B, right? and keyof this
will refer to key of B, right?
Upvotes: 2
Views: 2911
Reputation: 19947
First, you are "cheating".
abstract class A {
constructor() {
this.initialize()
// ~~~~~~~~~~ this `initialize` is from subclass
}
// different from this one here
initialize<T extends {
[t in keyof this]?: boolean
}>(todos?: T) {
// Do something
}
}
You just trick TS to think you're calling the same initialize
method, by give them the same name and compatible function signature (the optional todos
param).
What you're trying to do should really be written as:
abstract class A {
constructor() {
this.initialize()
}
// fix 1: delcare abstract method
abstract initialize(): void
// fix 2: rename, mark as protected
protected _initialize<T extends {
[t in keyof this]?: boolean
// fix 3: todos probably isn't optional
}>(todos: T) {
// Do something
}
}
class B extends A {
initialize() {
super._initialize({
test: false
})
}
test() {
return 'test'
}
}
It's one way. Subclasses know about base class, but base class is not obligated to know about all subclasses that extend itself. Because each subclass can implement something different, how is base class suppose to know all these info?
That's the whole point of the abstract class thing. You declare abstract method or property on base class, that's effectively a contract established by base class. Whichever subclass wants to extend me? Sign the contract first! Comply with my terms!
Back to your case, why is _initialize
in A
supposed to know any thing about B
? There could be C
or D
extending A
too. It's just not possible to infer the keyof
subclasses merely from this
.
So you need to tell super._initialize
about what subclass is this
when calling. It's a requirement, not a limit of TS.
abstract class A {
constructor() {
this.initialize()
}
abstract initialize(): void
// pass subclass type as generic param B
protected _initialize<B>(todos: {
[t in keyof B]: boolean
}) {
// Do something
}
}
class B extends A {
initialize() {
super._initialize<B>({
test: true, // <-- this is correct now
foobar: true // <-- this triggers error report
})
}
test() {
return 'test'
}
}
You can do a lot of hacky stuff in JS, but don't confuse JS with TS. TS is mostly about best practice, and you hit all sort of quirk and error when you break them. Your case is not a limitation of TS, that's TS trying to persuade you back to the right track.
Upvotes: 1
Reputation: 467
@JGoodgive How then i could have a real reference to current class? As i need it to point current class for later use in class' descendants. I can do this by:
abstract class A<Model> {
constructor() {
this.initialize()
}
initialize<T extends {
[t in keyof Model]?: boolean
}>(todos?: T) {
// Do something
}
}
class B extends A<B> {
initialize() {
super.initialize({
test: true,
initialize: false,
})
}
test() {
return 'test'
}
}
But it seems silly to pass in B into B, don't you think?
Upvotes: 0