Recipe · Classes

How do I type getters and setters in a JavaScript class?

  • Node.js (ES modules)
  • Getters
  • Setters
  • Classes
  • Checked with TypeScript 5.9

The problem My Product class has a price getter and a setter that also accepts a string like "39.00". Where do the JSDoc types go on accessors, and how do I keep the getter returning a number?

The short answer

Type the getter with @returns and the setter’s parameter with @param. The two may differ: here the setter also accepts a string, and the getter always returns a number:

product.js product.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 Product {
  #cents = 0;

  /** @returns {number} The price in cents. */
  get price() {
    return this.#cents;
  }

  /** @param {number | string} value Cents, or a string such as "39.00". */
  set price(value) {
    const cents = typeof value === "string" ? Math.round(Number.parseFloat(value) * 100) : value;
    if (!Number.isInteger(cents) || cents < 0) throw new RangeError(`Bad price: ${value}`);
    this.#cents = cents;
  }

  get label() {
    return `$${(this.#cents / 100).toFixed(2)}`;
  }
}

const lamp = new Product();
lamp.price = 3900;
lamp.price = "42.50";
console.log(lamp.price.toFixed(0), lamp.label.length);
export class Product {
  #cents = 0;

  get price(): number {
    return this.#cents;
  }

  set price(value: number | string) {
    const cents = typeof value === "string" ? Math.round(Number.parseFloat(value) * 100) : value;
    if (!Number.isInteger(cents) || cents < 0) throw new RangeError(`Bad price: ${value}`);
    this.#cents = cents;
  }

  get label(): string {
    return `$${(this.#cents / 100).toFixed(2)}`;
  }
}

const lamp = new Product();
lamp.price = 3900;
lamp.price = "42.50";
console.log(lamp.price.toFixed(0), lamp.label.length);
A getter and setter pair, and a getter on its own.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

lamp.price = "42.50" is allowed because the setter takes number | string, and lamp.price.toFixed(0) is allowed because the getter returns number. label has a getter and no setter, so it is read-only.

Why it works

TypeScript treats a get/set pair as one property with a read type (the getter’s return type) and a write type (the setter’s parameter type). Assignments are checked against the write type, reads get the read type. Since TypeScript 5.1 the two types may even be unrelated; here the read type, number, is part of the write type anyway.

The same thing in TypeScript

The TypeScript equivalent tab writes get price(): number and set price(value: number | string). Accessors are plain JavaScript, so the only change is where the types go.

What goes wrong

A value outside the setter’s type fails (TS2322), and assigning to a property that has only a getter fails as a read-only property (TS2540):

product.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: product.js(21,1): error TS2322: Type 'null' is not assignable to type 'string | number'. product.js(23,6): error TS2540: Cannot assign to 'label' because it is a read-only property. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. product.js(21,1): error TS2322: Type 'null' is not assignable to type 'string | number'. product.js(23,6): error TS2540: Cannot assign to 'label' because it is a read-only property. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. product.js(21,1): error TS2322: Type 'null' is not assignable to type 'string | number'. product.js(23,6): error TS2540: Cannot assign to 'label' because it is a read-only property. checked 21 October 2026
export class Product {
  #cents = 0;

  /** @returns {number} */
  get price() {
    return this.#cents;
  }

  /** @param {number | string} value */
  set price(value) {
    this.#cents = typeof value === "string" ? Number.parseFloat(value) * 100 : value;
  }

  get label() {
    return `$${(this.#cents / 100).toFixed(2)}`;
  }
}

const lamp = new Product();
lamp.price = null;error TS2322: Type 'null' is not assignable to type 'string | number'.
lamp.label = "$39.00";error TS2540: Cannot assign to 'label' because it is a read-only property.
A value the setter does not take, and an assignment to a getter alone.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • Write the getter’s type when the setter is wider. With no @returns, the getter’s type is inferred from its body; reading the property right after assigning a string through the setter can then come back typed string. A @returns on the getter avoids that surprise.
  • The JSDoc goes on each accessor. A comment above the getter does not type the setter; give each its own tag.
  • Accessors with #private storage are the common pair. The getter and setter expose a checked property while the #cents field stays inside the class (see private fields).

Questions

Can the setter accept a type that the getter does not return?

Yes, since TypeScript 5.1 the two can have unrelated types. Before 5.1 the getter's type had to be assignable to the setter's, which a number getter and a number | string setter already were.

Does a getter on its own make a property read-only?

Yes. With a get and no set, assigning to the property is TS2540, and in strict-mode JavaScript (every class body and module) the assignment also throws a TypeError at runtime.

Sources

Last reviewed 21 October 2026.