249

I am trying to figure out how to correctly define abstract methods in TypeScript:

Using the original inheritance example:

class Animal {
    constructor(public name) { }
    makeSound(input : string) : string;
    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name) { super(name); }
    makeSound(input : string) : string {
        return "sssss"+input;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}

I would like to know how to correctly define method makeSound, so it is typed and possible to overried.

Also, I am not sure how to define correctly protected methods - it seems to be a keyword, but has no effect and the code won't compile.

Carson
  • 6,105
  • 2
  • 37
  • 45
Vojtěch
  • 11,312
  • 31
  • 103
  • 173

5 Answers5

369

The name property is marked as protected. This was added in TypeScript 1.3 and is now firmly established.

The makeSound method is marked as abstract, as is the class. You cannot directly instantiate an Animal now, because it is abstract. This is part of TypeScript 1.6, which is now officially live.

abstract class Animal {
    constructor(protected name: string) { }

    abstract makeSound(input : string) : string;

    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }

    makeSound(input : string) : string {
        return "sssss"+input;
    }

    move() {
        alert("Slithering...");
        super.move(5);
    }
}

The old way of mimicking an abstract method was to throw an error if anyone used it. You shouldn't need to do this any more once TypeScript 1.6 lands in your project:

class Animal {
    constructor(public name) { }
    makeSound(input : string) : string {
        throw new Error('This method is abstract');
    }
    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

class Snake extends Animal {
    constructor(name) { super(name); }
    makeSound(input : string) : string {
        return "sssss"+input;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}
Lee Goddard
  • 10,680
  • 4
  • 46
  • 63
Fenton
  • 241,084
  • 71
  • 387
  • 401
  • Is it normal behaviour, that the compiler doesn't complain if I miss a parameter, change a parameters type or change the return type when overriding an abstract method? – Vetterjack Dec 05 '17 at 19:53
  • 1
    It is valid to either omit a parameter (if you don't use it, you can ignore any value that is passed) and you can have parameters of compatible types. You would get an error if you tried to implement an abstract method as `makeSound(input : number) : string {` based on the above example where `input` should be a string. `Type 'string' is not assignable to type 'number'.`. – Fenton Dec 06 '17 at 11:50
27

If you take Erics answer a little further you can actually create a pretty decent implementation of abstract classes, with full support for polymorphism and the ability to call implemented methods from the base class. Let's start with the code:

/**
 * The interface defines all abstract methods and extends the concrete base class
 */
interface IAnimal extends Animal {
    speak() : void;
}

/**
 * The abstract base class only defines concrete methods & properties.
 */
class Animal {

    private _impl : IAnimal;

    public name : string;

    /**
     * Here comes the clever part: by letting the constructor take an 
     * implementation of IAnimal as argument Animal cannot be instantiated
     * without a valid implementation of the abstract methods.
     */
    constructor(impl : IAnimal, name : string) {
        this.name = name;
        this._impl = impl;

        // The `impl` object can be used to delegate functionality to the
        // implementation class.
        console.log(this.name + " is born!");
        this._impl.speak();
    }
}

class Dog extends Animal implements IAnimal {
    constructor(name : string) {
        // The child class simply passes itself to Animal
        super(this, name);
    }

    public speak() {
        console.log("bark");
    }
}

var dog = new Dog("Bob");
dog.speak(); //logs "bark"
console.log(dog instanceof Dog); //true
console.log(dog instanceof Animal); //true
console.log(dog.name); //"Bob"

Since the Animal class requires an implementation of IAnimal it's impossible to construct an object of type Animal without having a valid implementation of the abstract methods. Note that for polymorphism to work you need to pass around instances of IAnimal, not Animal. E.g.:

//This works
function letTheIAnimalSpeak(animal: IAnimal) {
    console.log(animal.name + " says:");
    animal.speak();
}
//This doesn't ("The property 'speak' does not exist on value of type 'Animal')
function letTheAnimalSpeak(animal: Animal) {
    console.log(animal.name + " says:");
    animal.speak();
}

The main difference here with Erics answer is that the "abstract" base class requires an implementation of the interface, and thus cannot be instantiated on it's own.

Tiddo
  • 6,331
  • 6
  • 52
  • 85
  • 1
    For me at least with Typescript v1 - I can't reference 'this' from within a constructor to pass to super. Thoughts? – Kieran Benton May 18 '14 at 15:55
  • Which exact version of the compiler do you use, and what error do you get? tsc 1.0.1 compiles the above snippets perfectly fine. – Tiddo May 19 '14 at 07:49
  • The 'this' keyword is not allowed in super(). Im using tsc 1.0.3 – Zasz Oct 01 '14 at 11:15
  • That's pretty odd. Do you use the CLI compiler or Visual Studio? – Tiddo Oct 01 '14 at 13:44
  • I, too, cannot use "this" in the super() call. I can use it right after, to set the parent's member for the child implementation, but this does not enforce extension of the abstract class. I am using Palantir's Eclispe Typsscript plugin, v1.0.1. I notice that super(this) works fine in http://www.typescriptlang.org/Playground/ . – Eric Nov 04 '14 at 19:24
  • I played with this in the Playground and found out that `super(this)` works only if you are not initializing any class members inline. For example, if I declare and initialize `hello = 'world';` outside the constructor as a field I get an error with `super(this)` but not if I do `get hello() { return 'world'; }` or initialize it within the constructor. – Joe Skeen Apr 10 '15 at 21:43
4

I believe that using a combination of interfaces and base classes could work for you. It will enforce behavioral requirements at compile time (rq_ post "below" refers to a post above, which is not this one).

The interface sets the behavioral API that isn't met by the base class. You will not be able to set base class methods to call on methods defined in the interface (because you will not be able to implement that interface in the base class without having to define those behaviors). Maybe someone can come up with a safe trick to allow calling of the interface methods in the parent.

You have to remember to extend and implement in the class you will instantiate. It satisfies concerns about defining runtime-fail code. You also won't even be able to call the methods that would puke if you haven't implemented the interface (such as if you try to instantiate the Animal class). I tried having the interface extend the BaseAnimal below, but it hid the constructor and the 'name' field of BaseAnimal from Snake. If I had been able to do that, the use of a module and exports could have prevented accidental direct instantiation of the BaseAnimal class.

Paste this in here to see if it works for you: http://www.typescriptlang.org/Playground/

// The behavioral interface also needs to extend base for substitutability
interface AbstractAnimal extends BaseAnimal {
    // encapsulates animal behaviors that must be implemented
    makeSound(input : string): string;
}

class BaseAnimal {
    constructor(public name) { }

    move(meters) {
        alert(this.name + " moved " + meters + "m.");
    }
}

// If concrete class doesn't extend both, it cannot use super methods.
class Snake extends BaseAnimal implements AbstractAnimal {
    constructor(name) { super(name); }
    makeSound(input : string): string {
        var utterance = "sssss"+input;
        alert(utterance);
        return utterance;
    }
    move() {
        alert("Slithering...");
        super.move(5);
    }
}

var longMover = new Snake("windy man");

longMover.makeSound("...am I nothing?");
longMover.move();

var fulture = new BaseAnimal("bob fossil");
// compile error on makeSound() because it is not defined.
// fulture.makeSound("you know, like a...")
fulture.move(1);

I came across FristvanCampen's answer as linked below. He says abstract classes are an anti-pattern, and suggests that one instantiate base 'abstract' classes using an injected instance of an implementing class. This is fair, but there are counter arguments made. Read for yourself: https://typescript.codeplex.com/discussions/449920

Part 2: I had another case where I wanted an abstract class, but I was prevented from using my solution above, because the defined methods in the "abstract class" needed to refer to the methods defined in the matching interface. So, I tool FristvanCampen's advice, sort of. I have the incomplete "abstract" class, with method implementations. I have the interface with the unimplemented methods; this interface extends the "abstract" class. I then have a class that extends the first and implements the second (it must extend both because the super constructor is inaccessible otherwise). See the (non-runnable) sample below:

export class OntologyConceptFilter extends FilterWidget.FilterWidget<ConceptGraph.Node, ConceptGraph.Link> implements FilterWidget.IFilterWidget<ConceptGraph.Node, ConceptGraph.Link> {

    subMenuTitle = "Ontologies Rendered"; // overload or overshadow?

    constructor(
        public conceptGraph: ConceptGraph.ConceptGraph,
        graphView: PathToRoot.ConceptPathsToRoot,
        implementation: FilterWidget.IFilterWidget<ConceptGraph.Node, ConceptGraph.Link>
        ){
        super(graphView);
        this.implementation = this;
    }
}

and

export class FilterWidget<N extends GraphView.BaseNode, L extends GraphView.BaseLink<GraphView.BaseNode>> {

    public implementation: IFilterWidget<N, L>

    filterContainer: JQuery;

    public subMenuTitle : string; // Given value in children

    constructor(
        public graphView: GraphView.GraphView<N, L>
        ){

    }

    doStuff(node: N){
        this.implementation.generateStuff(thing);
    }

}

export interface IFilterWidget<N extends GraphView.BaseNode, L extends GraphView.BaseLink<GraphView.BaseNode>> extends FilterWidget<N, L> {

    generateStuff(node: N): string;

}
Eric
  • 663
  • 7
  • 18
2

I use to throw an exception in the base class.

protected abstractMethod() {
    throw new Error("abstractMethod not implemented");
}

Then you have to implement in the sub-class. The cons is that there is no build error, but run-time. The pros is that you can call this method from the super class, assuming that it will work :)

HTH!

Milton

Milton
  • 928
  • 1
  • 10
  • 22
-23

No, no, no! Please do not try to make your own 'abstract' classes and methods when the language does not support that feature; the same goes for any language feature you wish a given language supported. There is no correct way to implement abstract methods in TypeScript. Just structure your code with naming conventions such that certain classes are never directly instantiated, but without explicitly enforcing this prohibition.

Also, the example above is only going to provide this enforcement at run time, NOT at compile time, as you would expect in Java/C#.

rq_
  • 789
  • 1
  • 6
  • 6
  • 8
    I can see where you are coming from, but I respectfully disagree. If a language implements something, it's bad to re-implement it yourself. But if you don't have something, then you have no choice but to implement it yourself somehow. Sure, you won't see the issues until run-time, but throwing an exception the first time you test something will let you know you dun goofed pretty quickly. It's not ideal of course - which is why, IMO, Typescript needs abstract class support. Until it does however... – Maverick Jun 19 '14 at 07:09
  • I wished JavaScript had classes, type inference, static typing and interfaces, and guess what, Typescript has it. It would be the same for abstract method, the compiler just has to check that any class extending the abstract class implements the abstract method, as it already does for interfaces (an interface is essentially just a class with only abstract method) – Tony BenBrahim Feb 01 '15 at 11:40
  • 1
    I tend to agree with @rq_ here. The point of abstract methods is to get compile time validation that the program cannot get into an invalid state. The proposed solutions just give you runtime checks which means when your program runs you can't be sure it is in a valid state. This means you should be operating under the assumption that the method is not implemented, and guard accordingly. Lying to yourself that you have abstract methods is just asking to get bitten by unexpected runtime behavior. – Micah Zoltu Jun 21 '15 at 18:32