What polymorphism is
The word means "many forms". In object-oriented code it describes a single piece of code, such as a.speak(), that does different things depending on which kind of object a actually is. You write the call once, and the version that runs is chosen by the object. It builds directly on the last two lessons: inheritance gives Dog and Cat a common parent, and overriding gives each its own speak.
The setup
The starter code defines three classes. Animal is the parent, with a speak method that prints .... Dog and Cat extend it and override speak to print Woof and Meow. A Dog is an Animal, and so is a Cat, so TypeScript accepts either one wherever an Animal is expected.
Static type and run-time class
Two different ideas are at work, and keeping them apart is the key to this lesson.
- The declared type is what the compiler knows: a variable typed
Animalis only known to hold an animal. The compiler lets you call only whatAnimaloffers, such asspeak. - The actual class is what the object really is when the program runs: a
Dogor aCat.
When you call a.speak(), JavaScript looks at the actual object and runs the speak that its class provides. Types are erased during compilation, so the declared type never takes part in that decision. This is why the call can print Woof for one animal and Meow for another.
The exercise
The exercise puts Dogs and Cats in one collection and treats them alike.
- Build the collection. Declare a constant
animalswith the typeAnimal[], an array of animals, and fill it withnew Dog()andnew Cat(). Each subclass object fits the parent type, so both are accepted. From now on the code usinganimalsonly knows that it holds animals. - Loop and call. Use
for (const a of animals)and calla.speak()in the body. Each iteration gets one element, andconstis enough since the loop never reassignsa.
class Animal {
speak(): void {
console.log("...");
}
}
class Dog extends Animal {
speak(): void {
console.log("Woof");
}
}
class Cat extends Animal {
speak(): void {
console.log("Meow");
}
}
const animals: Animal[] = [new Dog(), new Cat()];
for (const a of animals) {
a.speak();
}
The output is:
Woof
Meow
The loop contains no if asking whether an element is a dog or a cat. The dispatch is automatic.
Why it matters
Suppose you later add a Cow that extends Animal and prints Moo. The loop doesn't change at all. Pushing a Cow into animals makes the same lines print Moo for it. Code written against the parent keeps working as new subclasses appear, and the same holds for a function that takes an Animal[] parameter. A subclass that doesn't override speak falls back to the parent's version, so a class that does nothing but extend Animal prints ....
Common mistakes
- Leaving off the parent type. Without the annotation, TypeScript infers the element type from the contents, a type built from the subclasses such as
Dog | Catrather thanAnimal. It would still work, but the lesson is about the common parent type, so writeAnimal[]. - Calling a child-only method on an
Animal. IfDoghad a methodfetch(), the compiler would rejecta.fetch()on anAnimal, since not every animal can fetch. Only members of the declared type can be called. - Renaming the override. If
Catdefinedmeow()instead ofspeak(), the cat would inheritAnimal'sspeakand print.... - Reading too much into the type. TypeScript compares object types by their members. An unrelated class with a matching
speak(): voidmethod is accepted as anAnimaltoo, and calling it works. Its object is notinstanceof Animal, because it does not extend the class. Rely on a shared parent when you want that connection to be explicit. - Casting to make code compile. A cast such as
as Animalchanges only what the compiler believes. If the object has nospeak, the call fails at run time with aTypeError.
Key takeaways
- Polymorphism lets one call, such as
a.speak(), run different code for different objects. - A subclass object can be stored wherever the parent type is expected, for instance in an
Animal[]. - The method that runs is chosen by the object's actual class at run time, not by the declared type.
- New subclasses work with existing code without any edits to it.