Why func(x2)
is valid? When x2.index
require F2
to be passed into it but func
only pass F1
class F1 {
value: string = ''
}
class F2 extends F1 {
value2: number = 0
}
abstract class X1 {
abstract index(param: F1): void
}
class X2 extends X1 {
override index(param: F2): void {
// Here [param] must be [F2]
console.log(`Is F2: ${param instanceof F2}`)
}
}
const func = (x: X1) => {
const f1 = new F1()
x.index(f1)
}
const x2 = new X2()
// Why [X2] can be passed here? No error?
// [X2.index] [param] should only accept [F2]
// But [func] will pass [F1] into it.
func(x2)
// The expected is that [func] should not be able to accept [X2].
func(x2) // Error expected
Despite seeming counter-intuitive, this is valid TypeScript code. The problem with it is that class X2
violates the Liskov substitution principle, which in short states that:
...an object (such as a class) may be replaced by a sub-object (such as a class that extends the first class) without breaking the program.
Specifically in this case, what breaks the aforementioned principle is the fact that X2
overrides X1.index(param: F1)
(OK to do) while changing its parameter to expect a derived type of F1
, which is F2
(becoming "not ok") and then accesses a field that exists only in that derived type (<- breaks the principle).
You can update your types and func
like below so that it throws an error if LSP is violated.
abstract class X1<TIndex extends F1> {
abstract index(param: TIndex): void;
}
class X2 extends X1<F2> {
override index(param: F2): void {
// Here [param] must be [F2]
console.log(`Is F2: ${param instanceof F2}`)
}
}
const func = <TIndex extends F1>(x: X1<TIndex>) => {
const f1 = new F1()
x.index(f1) // <-- throws error
}
Link to full code can be found in this playground.
(edit: inspiration taken from Sander van 't Einde's answer)