typescripttypescript-typings

Why this function can accept the class type when it's not compatible with the method parameter type


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

Playground


Solution

  • 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)