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:
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);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):
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.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 typedstring. A@returnson 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
#privatestorage are the common pair. The getter and setter expose a checked property while the#centsfield 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
- Classes (getters and setters) · TypeScript handbook
- TypeScript 5.1 release notes (unrelated types for getters and setters) · TypeScript handbook
- set · MDN Web Docs
Last reviewed 21 October 2026.