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:
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"));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:
/** @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);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:
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";
}
}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):
/** @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.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
@extendswithoutextendsdoes nothing. The tag cannot create inheritance; a mismatched one is TS8023.- Keep
@overrideon 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 usingthis; 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
- JSDoc Reference (@extends and @override) · TypeScript handbook
- TypeScript 4.3 release notes (override and noImplicitOverride) · TypeScript handbook
- Classes (inheritance) · TypeScript handbook
Last reviewed 21 October 2026.