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:
/** @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());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):
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 = [];
}
}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:
export class Cart {
coupon = null;
/** @param {string} code */
applyCoupon(code) {
this.coupon = code;error TS2322: Type 'string' is not assignable to type 'null'.
}
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
@typegoes 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.strictturns it on. A field that a method likeinit()sets later can say so with a default value, or with| undefinedin its type. - Parameter properties are TypeScript only.
constructor(private currency: string)has no JavaScript form; assignthis.currency = currencyinstead.
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
- Classes (fields and constructors) · TypeScript handbook
- Type Checking JavaScript Files (properties are inferred from assignments in class bodies) · TypeScript handbook
- Public class fields · MDN Web Docs
Last reviewed 21 October 2026.