What the capstone brings together
This exercise combines the ideas from the object-oriented chapter into one small program:
- Inheritance.
Dog,CatandBirdall extend a common parent,Animal. - A constructor in the parent.
Animalstores aname, and the subclasses reuse that constructor. - Overriding. The parent has a
sound()method that is only a placeholder. Each subclass replaces it with its own version. - Shared behaviour. The parent's
speak()callssound()and prints the result. Every subclass inheritsspeak()for free. - Polymorphism. An array typed
Animal[]holds a dog, a cat and a bird, and callingspeak()on each one runs the rightsound().
The shape is common in real programs: a small contract at the top, concrete variants below, and the rest of the code written against the parent type. Adding a new animal later doesn't require touching the loop that uses them.
The starting point
The starter code gives you the parent class:
class Animal {
name: string;
constructor(name: string) { this.name = name; }
sound(): string { throw new Error("Not implemented"); }
speak(): void {
console.log(this.name + ": " + this.sound());
}
}
Three things to read carefully. name: string; declares the field and its type, and the constructor assigns it. sound() is a placeholder that throws an error, so an animal that doesn't say what it sounds like fails loudly instead of printing something misleading. And speak() uses this.sound(): which version of sound runs depends on the actual object, not on the class where speak is written.
Step by step
- Write the three subclasses. Each one uses
extends Animaland overridessound(): stringto return its text:"Woof","Meow"and"Tweet". The return type matches the parent's, and TypeScript checks that an override is compatible with the method it replaces. - Skip the constructors. A subclass that defines no constructor receives the parent's, so
new Dog("Rex")already stores the name. You would need a constructor and a call tosuper(...)only if a subclass took extra arguments. - Build the array as
const animals: Animal[]with aDognamed Rex, aCatnamed Whiskers and aBirdnamed Tweety, in that order. The annotation says every element is treated as anAnimal, and each subclass qualifies because it extends it. - Loop with
for...of, naming the variablea(declared withconst), and calla.speak()in the body. The loop never asks which kind of animal it has. That is polymorphism.
The solution in TypeScript
class Animal {
name: string;
constructor(name: string) { this.name = name; }
sound(): string { throw new Error("Not implemented"); }
speak(): void {
console.log(this.name + ": " + this.sound());
}
}
class Dog extends Animal {
sound(): string { return "Woof"; }
}
class Cat extends Animal {
sound(): string { return "Meow"; }
}
class Bird extends Animal {
sound(): string { return "Tweet"; }
}
const animals: Animal[] = [new Dog("Rex"), new Cat("Whiskers"), new Bird("Tweety")];
for (const a of animals) {
a.speak();
}
Output:
Rex: Woof
Whiskers: Meow
Tweety: Tweet
How the dispatch works
On the first pass, a refers to the Dog. The call a.speak() finds no speak in Dog, so JavaScript looks in the parent and runs Animal.speak. Inside it, this is still the dog, so this.sound() finds Dog's own override first and returns "Woof". The same line of code in speak gives three different results because this is a different object each time.
Here is what happens if a subclass forgets to override. The placeholder runs:
class Fish extends Animal {}
new Fish("Nemo").speak();
This throws Error: Not implemented. The compiler accepts a Fish, because sound exists in the parent. If you want the compiler to reject it, you can mark the parent and its method abstract. Then every concrete subclass must define sound, and new Animal(...) is an error. The placeholder-and-throw style used here moves that check from compile time to run time.
Types are also erased when the code is compiled, so the annotation Animal[] changes nothing about how the program runs. It only controls what the compiler lets you write. For example, if Dog had an extra fetch() method, a.fetch() would be a compile error inside the loop, because a is typed as Animal, even though the object does have the method.
Common mistakes
- Wrong order in the array. The output follows the array: Rex, Whiskers, Tweety.
- Overriding with another return type. A
sound()that returns a number is reported as not assignable to the parent'ssound(): string. - Writing
speakin each subclass. It works, but duplicates the shared code and loses the point of inheritance. Onlysoundvaries. - Printing instead of returning in
sound. The parent'sspeakbuilds the line itself, sosoundmust return the text only. - Using
for...in. It would give array indexes as strings. Usefor...ofto get the elements. - Misspelling a name. The subclass method must be called exactly
sound, or it won't override anything and the placeholder throws instead.
Key takeaways
- Put shared data and behaviour in the parent, and the parts that vary in overridable methods.
- A subclass without its own constructor reuses the parent's.
- Calls on
thisrun the overriding method of the actual object, which is what makes one loop work for every kind of animal. - Code written against the parent type keeps working when new subclasses are added.