What an interface is
An interface is a contract: a list of methods that a class promises to provide. It says what an object can do, not how it does it and not what the object is. A Truck, a Robot and a Drone are different kinds of things, but all three can move(). The code that moves things only needs to know about that one method.
That is the difference from inheritance. Inheritance models "is a": a Dog is an Animal, and it receives the parent's fields and method bodies. An interface models "can do": it carries no data and no shared code, only the promise. A class has one parent at most, but it can make as many promises as it likes, so interfaces suit abilities that cut across the class tree, such as being comparable, serialisable or iterable.
JavaScript has no interface keyword
Languages like Java and C# have an interface keyword, and the compiler refuses a class that leaves a promised method out. Plain JavaScript has no such keyword (TypeScript adds one, but it disappears when the code is compiled to JavaScript), and nothing checks a class against a contract before the program runs. JavaScript programmers therefore use conventions. The simplest is a stub base class whose methods throw:
class Speaker {
speak() { throw new Error("Not implemented"); }
}
A class that wants to be a Speaker extends this class and overrides speak(). If it forgets, calling speak() reaches the stub and throws, so the missing method shows up as soon as it is used instead of as a silent wrong answer.
Why the stub throws
An empty body would be worse. A subclass that forgot to override speak() would run the empty method, do nothing, and look like it worked. Throwing makes the broken promise loud. Notice that the check happens at the call, not when the class is declared:
class Speaker {
speak() { throw new Error("Not implemented"); }
}
class Fish extends Speaker {}
const nemo = new Fish(); // no error here
try {
nemo.speak();
} catch (e) {
console.log(e.message); // Not implemented
}
Declaring Fish and creating a Fish both succeed. Only the call fails.
The exercise: a Dog that can speak
The exercise starts with Speaker already written, followed by two lines that create a Dog called rex and call rex.speak(). The expected output is Woof. The work is in two steps.
- Declare the class. Write
class Dog extends SpeakerbetweenSpeakerand the lines that use it. The class declaration has to come beforenew Dog()runs. Unlike functions, classes can't be used before their declaration is reached. - Override the method. Inside
Dog, definespeak()and make it logWoofwithconsole.log. This version replaces the one inherited fromSpeaker, so the stub that throws is never reached for aDog.
The finished program:
class Speaker {
speak() { throw new Error("Not implemented"); }
}
class Dog extends Speaker {
speak() {
console.log("Woof");
}
}
let rex = new Dog();
rex.speak();
It prints:
Woof
Gotchas in JavaScript
- Nothing enforces the contract early. A class that forgets a method is still accepted. The error appears only when that method is called, which might be in a rarely used branch of the program.
- The stub can be instantiated.
new Speaker()works and gives an object whosespeak()throws. If you want to forbid that, checknew.targetin the constructor and throw when it equals the base class. - A checker based on the prototype is fooled by the stub. Because
FishinheritsspeakfromSpeaker,typeof Fish.prototype.speak === "function"is true even thoughFishnever wrote its own version. To tell, compare against the stub:Fish.prototype.speak === Speaker.prototype.speak. - Spelling matters. Method names are plain strings. A subclass that defines
Speak()orspeek()overrides nothing, and the stub still throws. - A class extends only one class. The stub-class convention gives you a single contract per class. Capabilities can still be combined, as the next section shows.
Other ways to express a contract
Duck typing. Often no base class is needed at all. Any object with a speak method can be used where a speaker is expected, whatever its class. The code that consumes it just calls the method, or checks first:
const canSpeak = (o) => typeof o.speak === "function";
console.log(canSpeak({ speak() {} })); // true
console.log(canSpeak({})); // false
Several contracts on one class. A function that takes a base class and returns a subclass of it, often called a mixin, lets a class pick up several capabilities even though extends accepts only one parent:
const Swimmer = (Base) => class extends Base {
swim() { return "swim"; }
};
const Walker = (Base) => class extends Base {
walk() { return "walk"; }
};
class Animal {}
class Duck extends Walker(Swimmer(Animal)) {}
const d = new Duck();
console.log(d.swim(), d.walk()); // swim walk
Unlike a true interface, a mixin like this carries its code with it. The point is only that one class can end up with more than one capability.
Interface versus abstract class
An abstract class may hold fields and finished methods next to the stubs, and a subclass shares them. An interface holds none of that, only method names. In JavaScript the stub class blurs the two, because the same class syntax serves both. The habit that keeps them apart is a matter of what you put inside. If the base class has only throwing stubs, it plays the interface role. If it also stores data or has working methods that subclasses reuse, it is an abstract class.
Key takeaways
- An interface lists what a class can do, with no data and no shared code. Inheritance says what a class is.
- JavaScript has no
interfacekeyword. The usual convention is a base class whose methods throwError("Not implemented"). - A class implements the contract by extending the base class and overriding every method.
- A missing override is found when the method is called, not when the class is declared.
- Duck typing and mixins cover cases where one base class is not enough.