The short answer
Put square brackets around the parameter’s name in its @param tag. If the parameter has a default, write the default in the JavaScript itself:
/**
* @param {number} cents
* @param {string} [currency] The ISO code; "USD" when left out.
* @param {{ showCode?: boolean }} [options]
* @returns {string}
*/
export function formatPrice(cents, currency = "USD", options = {}) {
const text = new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
return options.showCode ? `${text} ${currency}` : text;
}
/**
* @param {string} name
* @param {string} [note]
* @returns {string}
*/
export function labelFor(name, note) {
return note ? `${name} (${note.trim()})` : name;
}
formatPrice(1999);
formatPrice(1999, "EUR");
formatPrice(1999, "EUR", { showCode: true });
labelFor("Desk lamp");export function formatPrice(cents: number, currency = "USD", options: { showCode?: boolean } = {}): string {
const text = new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
return options.showCode ? `${text} ${currency}` : text;
}
export function labelFor(name: string, note?: string): string {
return note ? `${name} (${note.trim()})` : name;
}
formatPrice(1999);
formatPrice(1999, "EUR");
formatPrice(1999, "EUR", { showCode: true });
labelFor("Desk lamp");jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} [currency] and [options] tell tsc that callers may leave those arguments out, so all three calls to formatPrice pass. [note] does the same for labelFor, which has no default.
Why it works
TypeScript reads a bracketed @param name as an optional parameter, the JSDoc spelling of note?: string. Inside the function an optional parameter without a default has the type string | undefined, which is why labelFor checks note before calling note.trim().
A JavaScript default value does more: it makes the parameter optional for callers and gives it a value when the argument is missing. Inside formatPrice, currency is a plain string and options is the object type, because the default fills the gap; no check is needed.
The same thing in TypeScript
Switch the first example to the TypeScript equivalent tab. A default becomes currency = "USD", with the type inferred from the value, and an optional parameter without one is note?: string. The two files describe the same functions to the checker.
What goes wrong
Leave the brackets off a typed parameter that has no default, and every call must pass it:
/**
* @param {number} cents
* @param {string} currency
* @returns {string}
*/
export function formatPrice(cents, currency) {
return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
}
formatPrice(1999);error TS2554: Expected 2 arguments, but got 1.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Untyped parameters in a .js file are all treated as optional. Once a @param line gives a parameter a type, it is required unless the name is in brackets or the parameter has a default, so calls that used to pass can fail with TS2554 when you add the JSDoc.
The other mistake is on the inside. An optional parameter can be undefined, so using it as if it were always there is TS18048:
/**
* @param {string} name
* @param {string} [note]
* @returns {string}
*/
export function labelFor(name, note) {
return `${name} (${note.trim()})`;error TS18048: 'note' is possibly 'undefined'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} A default written only in the JSDoc does not help. [note="no note"] reads well in a hover, but TypeScript does not apply it, so the function still sees undefined when the argument is missing:
/**
* @param {string} name
* @param {string} [note="no note"]
* @returns {string}
*/
export function labelFor(name, note) {
return `${name} (${note.trim()})`;error TS18048: 'note' is possibly 'undefined'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Gotchas
- Optional parameters come last. A required parameter after an optional one has to be passed anyway, so put the optional ones at the end, or collect them in one options object.
{string=}works too. The Closure Compiler form@param {string=} currencymeans the same as[currency]; pick one spelling per project.- Optional is not nullable.
[note]allowsundefinedbut notnull. If callers passnull, say so:@param {string | null} [note].
Questions
Do I still need the brackets when the parameter has a default value?
No. In a .js file a parameter with a default value (currency = "USD") is optional for callers whatever its @param line says. The brackets are still worth writing: they tell the next reader, and documentation tools, that the argument can be left out.
Is @param {string=} currency the same as [currency]?
Yes. The trailing = is the Closure Compiler spelling of an optional parameter, and TypeScript reads it the same way as square brackets around the name. Square brackets are the more common form today.
Can I pass undefined for an optional parameter?
Yes. An optional parameter accepts undefined, even with exactOptionalPropertyTypes on (that option is about object properties), and a default value applies when the argument is undefined or missing.
Sources
- JSDoc Reference (@param and @returns) · TypeScript handbook
- Type Checking JavaScript Files (optional parameters) · TypeScript handbook
- Default parameters · MDN Web Docs
Last reviewed 21 October 2026.