Override method with different argument types in extended class - Typescript
Asked Answered
J

4

24

I want to override a method and pass different argument types to it:

class Base {
    public myMethod(myString: string): undefined {
        return;
    }
}

class Child extends Base {
    public myMethod(myNumber: number): undefined {
        return super.myMethod(String(myNumber));
    }
}

Yet this yields a typescript error:

Property 'myMethod' in type 'Child' is not assignable to the same property in base type 'Base'. Type '(myNumber: number) => undefined' is not assignable to type '(myString: string) => undefined'. Types of parameters 'myNumber' and 'myString' are incompatible. Type 'string' is not assignable to type 'number'.

Is there a way to do this without creating a typescript error?

Julienne answered 6/6, 2018 at 20:57 Comment(3)
You can't, and shouldn't; you're violating the contract of the parent and the Liskov substitution principle.Rabblerouser
Can you explain what you are trying to accomplish? This might be one of those XY situations where the problem you need to solve has a solution other than attempting to override a method with an incompatible signature.Select
@Select This would have been only one solution, others are still viable for my problem. I just wondered if this would be possible as I didn't find documentation on this myself (yet all examples used this Liskov substitution principle)Julienne
V
15

There is not a way to do this*. In TypeScript, inheritance implies subtyping, so you can't inherit from a base class while not being a valid subtype of it.

* Technically not true but I'm not going to mention them because they're gross hacks that you shouldn't do.

Virilism answered 6/6, 2018 at 21:22 Comment(1)
well, even though I really don't like to acknowledge it, this is the only correct answerWonted
S
11

As mentioned by others this is not a good idea because you break Liskov substitution.

What you can easily do, is provide an override that takes both string and number. This allows your class to still be used wherever the base class is expected.

class Base {
     public myMethod(myString: string): undefined {
         return;
     }
 }

class Child extends Base {
    public myMethod(myNumberOrString: number | string): undefined {
        if (typeof myNumberOrString === 'number') {
            return super.myMethod(String(myNumberOrString));
        } else {
            return super.myMethod(myNumberOrString);
        }
    }
}
Saree answered 7/6, 2018 at 2:38 Comment(2)
any way how to make it work also for return types, not ony for parameters?Jamieson
@Jamieson Not really, for parameters you can get away with it because the derived class is the one handling the new parameter type. For return type, if your derived class returns a number and you use the derived class where the base class is expected (as you should be able to) the code that uses the base class has no way to deal with the unexpected return type. You can do some surgery on the type and create a derived class incompatible with the base class. But ask it as a different question, as that answer is a bit long for commentsSaree
R
4

It seems there is a way that works, even for return types, at least with TypeScript v 4.02 using an interim class.

But this is breaking Liskov substitution principle since it changes the return type and is not handling the case where the parameter is a string.

So it worths being mentioned just for the sake of knowledge, but in a code review I would not accept this hack, since a subclass should be able to replace the base class without breaking the functionality.

class Base {
    public myMethod(myString: string): string {
        return myString + myString;
    }
}

// weaken
// inspired by comment here: https://github.com/microsoft/TypeScript/issues/3402#issuecomment-385975990
class Interim extends Base {
    public myMethod(x: any): any { return super.myMethod(x); }
}

class Child extends Interim {
    public myMethod(myNumber: number): number {
        return myNumber * myNumber;
    }
}

// we can have a helper/utility for this
function weaken(klass: { new(...args: any[]): any; }, key: string) {
    return class extends klass {
        [key](...args: any[]) { super[key](...args); }
    }
}


class ChildOther extends weaken(Base, 'myMethod') {
    public myMethod(myNumber: number): number {
        return myNumber * myNumber;
    }
}

console.log((new Child()) instanceof Base); // true
console.log((new ChildOther()) instanceof Base); // true
console.log(new Base().myMethod('str')); // strstr
console.log(new Child().myMethod(3)); // 9
console.log(new ChildOther().myMethod(3)); // 9
Rosetta answered 2/10, 2020 at 9:40 Comment(2)
Thanks for sharing this, works like expected. I'm curious if you see any use case for this apart from documenting already existing code which can't be refactor due to some circumstances.Julienne
And while I think it answers my question and I upvoted it, I would leave the accepted "Don't do it... but it would be possible" answer. Those who really need the workaround will still find it after scrolling a bit.Julienne
E
-1

You CAN do this easily, as long as you create a third BaseClass and extend both of your classes from it. Just make sure the function with different parameters is defined ONLY in the extended classes (so they don't overlap). Here's a trivial example of it in action (my function that is different is called specialFunction):

    class BaseClass { // FirstClass and SecondClass both extend this class
        constructor() {
            this.name = ""
        }
        name: string
    }

    class FirstClass extends BaseClass {
        specialFunction(name: string) { // <- this only has one parameter
            this.name = name
        }
    }

    class SecondClass extends BaseClass {
        constructor() {
            super()
            this.num = 0
        }
        specialFunction(name: string, num: number) { // <- this has two parameters
            this.name = name
            this.num = num
        }
        num: number
    }
Empyrean answered 26/9, 2021 at 22:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.