Skip to content
TypeScript ts classes 4 min read

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" };
  }
}
ElementAllowed change in override
Return typeSame or narrower (subtype)
Parameter typesSame or wider (supertype)
Parameter countMay accept fewer (extra ignored)
VisibilityCannot loosen below the base

Turn on noImplicitOverride in 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 override on methods that replace a base-class implementation.
  • Enable noImplicitOverride in tsconfig.json to 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 override error as a signal that a base method was renamed or removed.
  • Do not loosen visibility in an override; it breaks substitutability.
  • Combine override with abstract base methods to guarantee every subclass implements them.
Last updated June 29, 2026
Was this helpful?