Recipe · Classes

How do I type a subclass in JavaScript with @extends and @override?

  • Node.js (ES modules)
  • @extends
  • @override
  • noImplicitOverride
  • Checked with TypeScript 5.9

The problem I have a base Notifier class and an EmailNotifier that extends it. Do I need @extends in JSDoc, how do I mark an overridden method, and what happens when the override does not match the base?

The short answer

Write class EmailNotifier extends Notifier as usual and mark each overriding method with @override. With noImplicitOverride on, tsc then checks that every override is marked and that every marked method really overrides something:

notifiers.js notifiers.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
export class Notifier {
  /**
   * @param {string} message
   * @returns {Promise<void>}
   */
  async send(message) {
    console.log(message);
  }
}

export class EmailNotifier extends Notifier {
  /** @param {string} address */
  constructor(address) {
    super();
    this.address = address;
  }

  /**
   * @override
   * @param {string} message
   */
  async send(message) {
    await fetch("https://mail.example/send", { method: "POST", body: JSON.stringify({ to: this.address, message }) });
  }
}

/** @param {Notifier} notifier */
export async function notifyShipped(notifier) {
  await notifier.send("Your order has shipped");
}

await notifyShipped(new EmailNotifier("ada@example.com"));
export class Notifier {
  async send(message: string): Promise<void> {
    console.log(message);
  }
}

export class EmailNotifier extends Notifier {
  address: string;

  constructor(address: string) {
    super();
    this.address = address;
  }

  override async send(message: string): Promise<void> {
    await fetch("https://mail.example/send", { method: "POST", body: JSON.stringify({ to: this.address, message }) });
  }
}

export async function notifyShipped(notifier: Notifier): Promise<void> {
  await notifier.send("Your order has shipped");
}

await notifyShipped(new EmailNotifier("ada@example.com"));
A subclass that overrides a method, marked with @override.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"],
    "noImplicitOverride": true
  },
  "include": ["*.js"]
}

notifyShipped accepts any Notifier, so an EmailNotifier passes, and its send is checked against the base class’s send.

Why it works

The extends keyword gives the subclass everything the base class has, and tsc requires each member the subclass redefines to stay compatible with the base, so code written for a Notifier keeps working with any subclass. @override is the JSDoc form of TypeScript’s override modifier: it documents the intent, and with noImplicitOverride it is required.

@extends has one job in JavaScript: passing a type argument to a generic base class. The tag says which T the subclass uses:

repositories.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/** @template T */
export class Repository {
  /** @type {Map<string, T>} */
  #items = new Map();

  /**
   * @param {string} id
   * @param {T} item
   */
  save(id, item) {
    this.#items.set(id, item);
  }

  /** @param {string} id */
  find(id) {
    return this.#items.get(id);
  }
}

/** @typedef {{ id: string, total: number }} Order */

/** @extends {Repository<Order>} */
export class OrderRepository extends Repository {
  /** @param {Order} order */
  add(order) {
    this.save(order.id, order);
  }
}

const orders = new OrderRepository();
orders.add({ id: "A-1001", total: 4000 });
console.log(orders.find("A-1001")?.total);
@extends gives a generic base class its type argument.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

The same thing in TypeScript

The TypeScript equivalent tab writes override async send(message: string). For a generic base, TypeScript puts the type argument in the clause itself, extends Repository<Order>, which is the line @extends stands in for.

What goes wrong

An override that changes the parameter type breaks the promise to callers of the base class; tsc reports it as TS2416 and spells out the mismatch. And with noImplicitOverride, an override without the tag is TS4119:

notifiers.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: notifiers.js(19,3): error TS2416: Property 'send' in type 'SmsNotifier' is not assignable to the same property in base type 'Notifier'. Type '(message: { to: string; text: string; }) => void' is not assignable to type '(message: string) => void'. Types of parameters 'message' and 'message' are incompatible. Type 'string' is not assignable to type '{ to: string; text: string; }'. notifiers.js(24,3): error TS4119: This member must have a JSDoc comment with an '@override' tag because it overrides a member in the base class 'Notifier'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. notifiers.js(19,3): error TS2416: Property 'send' in type 'SmsNotifier' is not assignable to the same property in base type 'Notifier'. Type '(message: { to: string; text: string; }) => void' is not assignable to type '(message: string) => void'. Types of parameters 'message' and 'message' are incompatible. Type 'string' is not assignable to type '{ to: string; text: string; }'. notifiers.js(24,3): error TS4119: This member must have a JSDoc comment with an '@override' tag because it overrides a member in the base class 'Notifier'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. notifiers.js(19,3): error TS2416: Property 'send' in type 'SmsNotifier' is not assignable to the same property in base type 'Notifier'. Type '(message: { to: string; text: string; }) => void' is not assignable to type '(message: string) => void'. Types of parameters 'message' and 'message' are incompatible. Type 'string' is not assignable to type '{ to: string; text: string; }'. notifiers.js(24,3): error TS4119: This member must have a JSDoc comment with an '@override' tag because it overrides a member in the base class 'Notifier'. checked 21 October 2026
export class Notifier {
  /** @param {string} message */
  send(message) {
    console.log(message);
  }

  /** @returns {string} */
  name() {
    return "console";
  }
}

export class SmsNotifier extends Notifier {
  /**
   * @override
   * @param {{ to: string, text: string }} message
   */
  send(message) {error TS2416: Property 'send' in type 'SmsNotifier' is not assignable to the same property in base type 'Notifier'.
  Type '(message: { to: string; text: string; }) => void' is not assignable to type '(message: string) => void'.
    Types of parameters 'message' and 'message' are incompatible.
      Type 'string' is not assignable to type '{ to: string; text: string; }'.
    console.log(message.to, message.text);
  }

  name() {error TS4119: This member must have a JSDoc comment with an '@override' tag because it overrides a member in the base class 'Notifier'.
    return "sms";
  }
}
An override that changes the parameter type, and one without @override.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": [],
    "noImplicitOverride": true
  },
  "include": ["*.js"]
}

Leave the @extends off a subclass of a generic class, and strict (through noImplicitAny) asks for it by name (TS8026):

repositories.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: repositories.js(13,38): error TS8026: Expected Repository<T> type arguments; provide these with an '@extends' tag. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. repositories.js(13,38): error TS8026: Expected Repository<T> type arguments; provide these with an '@extends' tag. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. repositories.js(13,38): error TS8026: Expected Repository<T> type arguments; provide these with an '@extends' tag. checked 21 October 2026
/** @template T */
export class Repository {
  /** @type {T[]} */
  items = [];

  /** @param {T} item */
  save(item) {
    this.items.push(item);
  }
}

export class OrderRepository extends Repository {}error TS8026: Expected Repository<T> type arguments; provide these with an '@extends' tag.
A generic base class extended without @extends.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • @extends without extends does nothing. The tag cannot create inheritance; a mismatched one is TS8023.
  • Keep @override on its own line in the comment above the method. A JSDoc comment on the same line as the class’s opening brace is not attached to the method after it.
  • Constructors do not override. A subclass constructor must call super(…) before using this; it needs no @override.

Questions

When is @extends needed?

Only when the base class is generic and you need to say its type argument, as in @extends {Repository<Order>}. For a plain base class the extends keyword is all tsc needs.

Is @override checked without noImplicitOverride?

Partly. An @override on a method that overrides nothing is reported in every config (TS4122). A missing @override is reported only with noImplicitOverride on (TS4119).

Sources

Last reviewed 21 October 2026.