Recipe · Classes

How do I make a JavaScript class implement an interface?

  • Node.js (ES modules)
  • @implements
  • @typedef
  • Classes
  • Checked with TypeScript 5.9

The problem Several classes in my app store orders: one in memory for tests, one in a database. I want tsc to check that each one has the methods the rest of the code calls. How do I declare that in JavaScript?

The short answer

Describe the interface as a @typedef (or import one), and put @implements {OrderStore} in the class’s JSDoc:

stores.js stores.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
/** @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());
A class that promises to match an interface with @implements.
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):

stores.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: stores.js(11,14): error TS2420: Class 'ReadOnlyStore' incorrectly implements interface 'OrderStore'. Property 'save' is missing in type 'ReadOnlyStore' but required in type 'OrderStore'. stores.js(22,9): 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'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. stores.js(11,14): error TS2420: Class 'ReadOnlyStore' incorrectly implements interface 'OrderStore'. Property 'save' is missing in type 'ReadOnlyStore' but required in type 'OrderStore'. stores.js(22,9): 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'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. stores.js(11,14): error TS2420: Class 'ReadOnlyStore' incorrectly implements interface 'OrderStore'. Property 'save' is missing in type 'ReadOnlyStore' but required in type 'OrderStore'. stores.js(22,9): 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'. checked 21 October 2026
/** @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);
  }
}
A missing method, and a method whose parameter type does not fit.
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. @implements checks the class but does not give its methods’ parameters their types; each method still needs its own @param tags, as in the examples.
  • Properties count too. A @typedef with 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>} load describes 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

Last reviewed 21 October 2026.