@property {Type} name (or @prop) declares one property of the object type a @typedef names. The lines together build the type, much like the members of an interface, and each line's text becomes the property's description in editor hovers.
TypeScript reads @property only inside a @typedef block. In the JSDoc comment of a class it is ignored, as the third example shows; declare class properties in the class body instead.
At a glance
| Use | JSDoc in a .js file | TypeScript in a .ts file |
|---|---|---|
| A property of a @typedef | /**
* @typedef {object} Address
* @property {string} street
*/ | interface Address {
street: string;
} |
| Optional | * @property {string} [unit] | unit?: string; |
| Nested | * @property {object} geo
* @property {number} geo.lat | geo: { lat: number }; |
Examples
/**
* @typedef {object} Address
* @property {string} street
* @property {string} [unit] apartment or suite
* @property {string} city
* @prop {string} postalCode
* @property {object} [geo]
* @property {number} geo.lat
* @property {number} geo.lng
*/
/** @type {Address} */
const office = {
street: "12 Market St",
city: "Springfield",
postalCode: "62701",
geo: { lat: 39.8, lng: -89.65 },
};
/** @param {Address} address */
export function label(address) {
return [address.street, address.unit, address.city].filter(Boolean).join(", ");
}
label(office);interface Address {
street: string;
/** apartment or suite */
unit?: string;
city: string;
postalCode: string;
geo?: { lat: number; lng: number };
}
const office: Address = {
street: "12 Market St",
city: "Springfield",
postalCode: "62701",
geo: { lat: 39.8, lng: -89.65 },
};
export function label(address: Address) {
return [address.street, address.unit, address.city].filter(Boolean).join(", ");
}
label(office);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @typedef {object} Address
* @property {string} street
* @property {string} [unit]
*/
/** @param {Address} address */
export function unitLabel(address) {
return address.unit.toUpperCase();error TS18048: 'address.unit' is possibly 'undefined'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @property {string} sku
*/
export class Product {}
new Product().sku;error TS2339: Property 'sku' does not exist on type 'Product'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Optional and nested properties
Brackets make a property optional: @property {string} [unit] has the type string | undefined when read, so it must be checked before use (TS18048 otherwise, as the second example shows).
Dotted names describe an object inside the object: @property {object} [geo] followed by @property {number} geo.lat. For anything deeper, a separate @typedef for the inner object is easier to read.
The one-line alternative
/** @typedef {{ street: string, unit?: string }} Address */ declares the same type without @property lines. The multi-line form is worth it when properties need descriptions.
Gotchas
- Another tag in the middle of the
@propertylines ends the type there. A@readonlyline, for example, drops every property after it; see the@readonlypage. @propertyon a class comment is ignored:/** @property {string} sku */ class Product {}adds nothing, andnew Product().skuis TS2339.
Questions
What is the difference between @property and @prop?
None. TypeScript treats them as the same tag.
Sources
- JSDoc Reference: @typedef and @property · TypeScript handbook
- @property · JSDoc
Last reviewed 20 October 2026.