The short answer
Describe the interface as a @typedef (or import one), and put @implements {OrderStore} in the class’s JSDoc:
/** @typedef {{ id: string, total: number }} Order */
/**
* @typedef {object} OrderStore
* @property {(id: string) => Promise<Order | undefined>} load
* @property {(order: Order) => Promise<void>} save
*/
/** @implements {OrderStore} */
export class MemoryOrderStore {
/** @type {Map<string, Order>} */
#orders = new Map();
/** @param {string} id */
async load(id) {
return this.#orders.get(id);
}
/** @param {Order} order */
async save(order) {
this.#orders.set(order.id, order);
}
}
/** @param {OrderStore} store */
export async function markPaid(store) {
const order = await store.load("A-1001");
if (order) await store.save({ ...order, total: 0 });
}
await markPaid(new MemoryOrderStore());type Order = { id: string; total: number };
interface OrderStore {
load(id: string): Promise<Order | undefined>;
save(order: Order): Promise<void>;
}
export class MemoryOrderStore implements OrderStore {
#orders = new Map<string, Order>();
async load(id: string) {
return this.#orders.get(id);
}
async save(order: Order) {
this.#orders.set(order.id, order);
}
}
export async function markPaid(store: OrderStore) {
const order = await store.load("A-1001");
if (order) await store.save({ ...order, total: 0 });
}
await markPaid(new MemoryOrderStore());jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} tsc checks MemoryOrderStore against OrderStore where the class is written, and markPaid works with any class that passes the same check.
Why it works
@implements is the JSDoc form of TypeScript’s implements clause. It does not change the class or add anything to it; it asks tsc to compare the class’s instance type with the named type and report what does not fit. The rest of the program is unaffected: TypeScript checks object types by shape, so the class would be accepted as an OrderStore even without the tag.
The same thing in TypeScript
The TypeScript equivalent tab declares interface OrderStore and writes class MemoryOrderStore implements OrderStore. JavaScript has no interface or implements syntax (both are errors in a .js file), so the type is a @typedef and the promise is a tag.
What goes wrong
A missing method is reported on the class (TS2420), and a method whose parameter type does not fit is reported on the method (TS2416):
/** @typedef {{ id: string, total: number }} Order */
/**
* @typedef {object} OrderStore
* @property {(id: string) => Promise<Order | undefined>} load
* @property {(order: Order) => Promise<void>} save
*/
/** @implements {OrderStore} */
export class ReadOnlyStore {error TS2420: Class 'ReadOnlyStore' incorrectly implements interface 'OrderStore'.
Property 'save' is missing in type 'ReadOnlyStore' but required in type 'OrderStore'.
/** @param {string} id */
async load(id) {
return { id, total: 0 };
}
}
/** @implements {OrderStore} */
export class BrokenStore {
/** @param {number} id */
async load(id) {error TS2416: Property 'load' in type 'BrokenStore' is not assignable to the same property in base type 'OrderStore'.
Type '(id: number) => Promise<undefined>' is not assignable to type '(id: string) => Promise<Order | undefined>'.
Types of parameters 'id' and 'id' are incompatible.
Type 'string' is not assignable to type 'number'.
return undefined;
}
/** @param {Order} order */
async save(order) {
console.log(order.id);
}
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- The tag does not type the methods.
@implementschecks the class but does not give its methods’ parameters their types; each method still needs its own@paramtags, as in the examples. - Properties count too. A
@typedefwith a data property, such as@property {string} name, requires the class to have that property. - Method syntax or property syntax. In the
@typedef,@property {(id: string) => Promise<Order | undefined>} loaddescribes a method;{{ load(id: string): Promise<Order | undefined> }}in one braced type works too.
Questions
Is @implements needed for a class to be accepted where the type is expected?
No. TypeScript compares shapes, so any object with the right methods fits an OrderStore parameter. @implements moves the check to the class itself, where a mistake is easier to read.
Can a class implement several types?
Yes, with one @implements tag per type, each on its own line of the class comment.
Sources
- JSDoc Reference (@implements) · TypeScript handbook
- Classes (implements clauses) · TypeScript handbook
Last reviewed 21 October 2026.