Method Overriding
Method overriding is when a subclass provides its own implementation of a method that already exists on its base class. At runtime the most-derived version wins, which is the engine behind polymorphism. The danger is silent mistakes: rename or remove a base method and your “override” quietly becomes a brand-new method that never runs. TypeScript 4.3 introduced the override keyword and the noImplicitOverride compiler flag to catch exactly these errors. This page covers how overriding works, the override keyword, the compatibility rules overrides must follow, and how noImplicitOverride enforces discipline across a codebase.
Basic overriding
To override, declare a method in the subclass with the same name as one in the base class. The subclass version replaces the base version for instances of the subclass.
class Animal {
speak(): string {
return "...";
}
}
class Dog extends Animal {
speak(): string {
return "Woof";
}
}
const pet: Animal = new Dog();
pet.speak(); // "Woof" — the Dog version runs
Even though pet is typed as Animal, the runtime dispatches to Dog.speak. This dynamic dispatch is what makes a Animal[] of mixed subclasses behave polymorphically.
The override keyword
Adding override before a method declares your intent: “this method is meant to replace one from the base class.” If no matching base method exists, the compiler errors — turning a silent bug into a build failure.
class Base {
greet(): string {
return "hi";
}
}
class Derived extends Base {
override greet(): string {
return "hello";
}
// ❌ Error: This member cannot have an 'override' modifier
// because it is not declared in the base class 'Base'.
override farewell(): string {
return "bye";
}
}
override greet() compiles because Base.greet exists. override farewell() fails because there is nothing to override — most likely a typo or a stale name.
Catching renamed base methods
The real payoff comes when the base class changes. With override, renaming a base method immediately flags every subclass that thought it was overriding it.
class Repository {
// method renamed from `fetch` to `load`
load(id: string): string {
return id;
}
}
class UserRepository extends Repository {
// ❌ Error: This member cannot have an 'override' modifier because
// it is not declared in the base class. (base method was renamed)
override fetch(id: string): string {
return `user:${id}`;
}
}
Without override, fetch would silently become a new, never-called method and load would fall back to the base implementation — a bug that compiles cleanly. The keyword surfaces it instantly.
Enforcing it with noImplicitOverride
The override keyword is optional by default. Enable noImplicitOverride in tsconfig.json to require it: any method that does override a base method but omits the keyword becomes an error.
{
"compilerOptions": {
"noImplicitOverride": true
}
}
class Base {
run(): void {}
}
class Job extends Base {
// ❌ Error: This member must have an 'override' modifier because it
// overrides a member in the base class 'Base'.
run(): void {}
}
With the flag on, the entire team is forced to annotate every override, making accidental overrides (and accidental non-overrides) impossible to merge unnoticed.
Compatibility rules for overrides
An override must remain type-compatible with the method it replaces so the subtype stays substitutable. Parameters may be the same or wider (contravariant) and the return type the same or narrower (covariant).
class Producer {
make(): { id: number } {
return { id: 1 };
}
}
class DetailedProducer extends Producer {
// ✅ narrower return type is allowed
override make(): { id: number; name: string } {
return { id: 1, name: "x" };
}
}
class Broken extends Producer {
// ❌ Error: incompatible — return type is missing 'id'
override make(): { name: string } {
return { name: "x" };
}
}
| Element | Allowed change in override |
|---|---|
| Return type | Same or narrower (subtype) |
| Parameter types | Same or wider (supertype) |
| Parameter count | May accept fewer (extra ignored) |
| Visibility | Cannot loosen below the base |
Turn on
noImplicitOverridein every project. The cost is one keyword per override; the payoff is the compiler catching an entire class of refactoring bugs for you.
Best Practices
- Always write
overrideon methods that replace a base-class implementation. - Enable
noImplicitOverrideintsconfig.jsonto enforce the keyword team-wide. - Use
super.method()inside an override when you want to extend, not replace, behavior. - Keep override signatures compatible: narrower returns, wider parameters.
- Treat an unexpected
overrideerror as a signal that a base method was renamed or removed. - Do not loosen visibility in an override; it breaks substitutability.
- Combine
overridewithabstractbase methods to guarantee every subclass implements them.