TypeScript 4.3: Mastering the override Keyword

Felipe Hlibco

TypeScript 4.3 RC dropped on May 11th, and I’ve been running it on a side project for the past two weeks. The headline feature — the override keyword — fills a gap that’s bitten me more than once in large codebases.

Here’s the scenario. You’ve got a base class. You override a method in a subclass. Six months later, someone renames the base method during a refactor. Your subclass method silently becomes a new method instead of an override. No error. No warning. Just a subtle bug that passes type checking and breaks at runtime.

The override keyword fixes this.

How override Works #

Mark a method with override and TypeScript verifies that a method with the same name exists on a base class. If it doesn’t — someone renamed it, deleted it, or you made a typo — you get a compile-time error.

class Animal {
  move(distance: number) {
    console.log(`Moved ${distance} meters`);
  }
}

class Dog extends Animal {
  override move(distance: number) {
    console.log(`Ran ${distance} meters`);
  }

  // Error: This member cannot have an 'override' modifier
  // because it is not declared in the base class 'Animal'
  override speak() {
    console.log('Woof!');
  }
}

If Animal.move gets renamed to Animal.travel, the override move() in Dog immediately errors out. That’s the value. The compiler catches the disconnect before your tests (or your users) do.

The –noImplicitOverride Flag #

The keyword by itself is opt-in. You can add override where you want and skip it elsewhere. But for teams that want to enforce it across the codebase, --noImplicitOverride makes it mandatory.

With this flag enabled, any method that overrides a base class method must use the override keyword. Forget it, and TypeScript errors. This prevents accidental overrides — cases where a subclass method happens to have the same name as a base method but wasn’t intended as an override.

// tsconfig.json
{
  "compilerOptions": {
    "noImplicitOverride": true
  }
}

I’d recommend turning this on for any project with non-trivial class hierarchies. The migration cost is low: TypeScript tells you exactly which methods need the override annotation, and adding it’s a mechanical change. The ongoing benefit is that every override is explicit and every accidental name collision gets caught.

If you’re working with class hierarchies in frameworks like NestJS or TypeORM, this is particularly valuable. Those frameworks use inheritance extensively, and a silent override failure can cause confusing behavior — a lifecycle hook that stops firing, a query method that returns wrong results.

Separate Read/Write Types on Properties #

The second feature I’ve been using is type divergence between getters and setters. Before 4.3, get and set accessors had to agree on a single type. Now they can differ:

class Config {
  private _values: Map<string, string> = new Map();

  // Accepts string | string[] for convenience
  set items(value: string | string[]) {
    const arr = Array.isArray(value) ? value : [value];
    arr.forEach(v => this._values.set(v, v));
  }

  // Always returns string[]
  get items(): string[] {
    return [...this._values.values()];
  }
}

const config = new Config();
config.items = 'single-value';     // OK: string accepted
config.items = ['a', 'b', 'c'];   // OK: string[] accepted
const result: string[] = config.items; // OK: always returns string[]

This pattern shows up frequently in configuration objects and builder APIs where you want a lenient setter (accept multiple formats) and a strict getter (always return a normalized type). Before 4.3, you had to use method-based APIs instead of property accessors to get this behavior.

Template String Type Improvements #

TypeScript 4.2 introduced template literal types. 4.3 improves how they interact with inference and pattern matching:

type EventName<T extends string> = `${T}Changed`;

function onEvent<T extends string>(
  event: EventName<T>,
  callback: (value: unknown) => void
): void { /* ... */ }

// TypeScript infers T = "name" from "nameChanged"
onEvent('nameChanged', (value) => { /* ... */ });

The inference works in both directions now — TypeScript can decompose a template literal type to extract the constituent parts. It’s one of those features that feels niche until you’re building a strongly-typed event system or API client, and then it’s transformative.

Static Index Signatures #

Classes can now have static index signatures, which is useful for classes that act as registries or dictionaries at the class level:

class Registry {
  static [key: string]: unknown;

  static register(name: string, value: unknown) {
    Registry[name] = value;
  }
}

A smaller feature, but it closes an asymmetry between class static members and object types that occasionally forced workarounds.

Upgrading #

The RC has been stable for me. I’ve hit no regressions in a ~40k line TypeScript project. The main migration task (if you enable --noImplicitOverride) is annotating existing overrides, which is tedious but trivial.

My suggestion: enable --noImplicitOverride from the start on new projects. Retrofit it on existing ones when you have a quiet sprint. The feature pays for itself the first time it catches a refactoring mistake that would’ve otherwise slipped through code review.

TypeScript continues to ship practical features. Not everything needs to be a type system breakthrough; sometimes the most valuable addition is a keyword that prevents a class of bugs you’ve seen three times before.