Type expression

Readonly<T>

  • Utility types
  • Tested with TypeScript 5.9

Readonly<Product> is Product with every property marked readonly. Assigning to one is TS2540 ("Cannot assign to 'price' because it is a read-only property"), as the first example shows. On a parameter, it tells callers that the function will not change the object they pass.

TypeScript has had Readonly since version 2.1. It exists only for TypeScript's checker; Object.freeze is the run-time counterpart and returns a Readonly type.

At a glance

JSDoc in a .js file TypeScript in a .ts file
/** @type {Readonly<Product>} */ : Readonly<Product>

Examples

products.js products.ts Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: products.js(6,11): error TS2540: Cannot assign to 'price' because it is a read-only property. products.ts(5,11): error TS2540: Cannot assign to 'price' because it is a read-only property. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. products.js(6,11): error TS2540: Cannot assign to 'price' because it is a read-only property. products.ts(5,11): error TS2540: Cannot assign to 'price' because it is a read-only property. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. products.js(6,11): error TS2540: Cannot assign to 'price' because it is a read-only property. products.ts(5,11): error TS2540: Cannot assign to 'price' because it is a read-only property. checked 21 October 2026
/** @typedef {{ sku: string, price: number, tags: string[] }} Product */

/** @param {Readonly<Product>} product */
export function discounted(product) {
  product.price = product.price * 0.8;error TS2540: Cannot assign to 'price' because it is a read-only property.
  return product;
}
type Product = { sku: string; price: number; tags: string[] };

export function discounted(product: Readonly<Product>) {
  product.price = product.price * 0.8;error TS2540: Cannot assign to 'price' because it is a read-only property.
  return product;
}
Readonly<T> on a parameter.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
products.js 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 {{ sku: string, tags: string[] }} Product */

/** @param {Readonly<Product>} product */
export function tagSale(product) {
  // Allowed: Readonly covers product.tags, not the array it points to.
  product.tags.push("sale");
  return product;
}
Readonly<T> is shallow.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Only one level deep

Readonly covers the object's own properties, not the objects they point to. In the second example product.tags.push("sale") passes, because tags itself was not reassigned. Type nested arrays as readonly string[] when they should not change either.

Gotchas

  • A Readonly<T> value can be passed where a plain T is expected; TypeScript does not treat readonly properties as incompatible with mutable ones in assignments.
  • For arrays, Readonly<string[]> is the same as readonly string[].

Questions

How do I make an object parameter read-only in JSDoc?

Type it @param {Readonly<Product>} product. Nested arrays and objects need their own readonly types.

Sources

Last reviewed 20 October 2026.