Recipe · Classes

How do I type class fields and the constructor in JavaScript?

  • Node.js (ES modules)
  • Classes
  • @type
  • @param
  • strictPropertyInitialization
  • Checked with TypeScript 5.9

The problem My JavaScript classes have fields declared at the top and others set in the constructor. Where does the JSDoc go, and why does tsc complain about an empty array field or a field that starts as null?

The short answer

Put a @type comment on each field whose initial value does not say enough, and type the constructor’s parameters with @param; a property assigned in the constructor takes the parameter’s type:

cart.js cart.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 {{ sku: string, quantity: number }} Line */

export class Cart {
  /** @type {Line[]} */
  items = [];

  /** @type {string | null} */
  coupon = null;

  /**
   * @param {string} currency ISO code, such as "USD".
   */
  constructor(currency) {
    this.currency = currency;
  }

  /**
   * @param {string} sku
   * @param {number} [quantity]
   */
  add(sku, quantity = 1) {
    this.items.push({ sku, quantity });
    return this;
  }
}

const cart = new Cart("USD").add("LAMP-01").add("DESK-02", 2);
console.log(cart.currency.toLowerCase(), cart.items.length, cart.coupon?.trim());
type Line = { sku: string; quantity: number };

export class Cart {
  items: Line[] = [];

  coupon: string | null = null;

  currency: string;

  constructor(currency: string) {
    this.currency = currency;
  }

  add(sku: string, quantity = 1): this {
    this.items.push({ sku, quantity });
    return this;
  }
}

const cart = new Cart("USD").add("LAMP-01").add("DESK-02", 2);
console.log(cart.currency.toLowerCase(), cart.items.length, cart.coupon?.trim());
Fields with initializers, a field set in the constructor, a typed empty array.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

items is a Line[] even though it starts empty, coupon can later hold a string, and currency is a string because the constructor assigns a string to it.

Why it works

In a .js file, TypeScript reads the class the way it runs. A field with an initializer takes its type from the value, unless a @type comment says otherwise. A property first assigned in the constructor (this.currency = currency) is declared by that assignment. The @param tags on the constructor type the arguments of new Cart(…), and every method’s JSDoc types the method.

The same thing in TypeScript

The TypeScript equivalent tab declares every field with a type annotation, including currency: string, which TypeScript requires before the constructor can assign it. JavaScript gets the same information from the @type comments and the constructor.

What goes wrong

An empty array tells tsc nothing about what it will hold, so a field that only ever gets [] is an implicit any[] (TS7008). And with strict on, a field declared with a type but never given a value is reported, because reading it would give undefined (TS2564):

cart.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: cart.js(3,3): error TS7008: Member 'items' implicitly has an 'any[]' type. cart.js(7,3): error TS2564: Property 'currency' has no initializer and is not definitely assigned in the constructor. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. cart.js(3,3): error TS7008: Member 'items' implicitly has an 'any[]' type. cart.js(7,3): error TS2564: Property 'currency' has no initializer and is not definitely assigned in the constructor. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. cart.js(3,3): error TS7008: Member 'items' implicitly has an 'any[]' type. cart.js(7,3): error TS2564: Property 'currency' has no initializer and is not definitely assigned in the constructor. checked 21 October 2026
export class Cart {
  items;error TS7008: Member 'items' implicitly has an 'any[]' type.

  /** @type {string} */
  currency;error TS2564: Property 'currency' has no initializer and is not definitely assigned in the constructor.

  constructor() {
    this.items = [];
  }
}
An untyped empty array, and a typed field nothing ever sets.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

A field that starts as null is the other classic. Without a @type, its type is null alone, so assigning a string later fails (TS2322). The @type {string | null} in the first example is the fix:

cart.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: cart.js(7,5): error TS2322: Type 'string' is not assignable to type 'null'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. cart.js(7,5): error TS2322: Type 'string' is not assignable to type 'null'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. cart.js(7,5): error TS2322: Type 'string' is not assignable to type 'null'. checked 21 October 2026
export class Cart {
  coupon = null;

  /** @param {string} code */
  applyCoupon(code) {
    this.coupon = code;error TS2322: Type 'string' is not assignable to type 'null'.
  }
}
A field that starts as null keeps only null without a @type.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • @type goes on its own line above the field. A JSDoc comment on the same line as the class’s opening brace is not attached to the field after it.
  • TS2564 needs strictPropertyInitialization. strict turns it on. A field that a method like init() sets later can say so with a default value, or with | undefined in its type.
  • Parameter properties are TypeScript only. constructor(private currency: string) has no JavaScript form; assign this.currency = currency instead.

Questions

Do I need a field declaration for every property?

No. In a .js file, this.currency = currency in the constructor declares the property, typed from the value. A field declaration at the top of the class is clearer, and it is where a @type goes when the value alone is not enough.

Does @property in the class comment declare fields?

No. TypeScript ignores @property in a class's JSDoc; it only reads @property inside a @typedef. Declare the fields in the class body instead.

Sources

Last reviewed 21 October 2026.