Recipe · Objects and data

How do I type an options object with default values in JSDoc?

  • Node.js (ES modules)
  • @typedef
  • @property
  • Options objects
  • Spread
  • Checked with TypeScript 5.9

The problem My function takes an options object where every property is optional and has a default. How do I write the type once, apply the defaults, and still get each option checked?

The short answer

Describe the options once with a @typedef whose properties are optional (names in brackets), mark the parameter itself optional, and destructure it with defaults:

export.js export.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 {object} ExportOptions
 * @property {"csv" | "json"} [format] Defaults to "csv".
 * @property {string} [delimiter] Defaults to ",".
 * @property {boolean} [header] Defaults to true.
 */

/**
 * @param {{ sku: string, quantity: number }[]} rows
 * @param {ExportOptions} [options]
 * @returns {string}
 */
export function exportStock(rows, { format = "csv", delimiter = ",", header = true } = {}) {
  if (format === "json") return JSON.stringify(rows);
  const lines = rows.map((row) => `${row.sku}${delimiter}${row.quantity}`);
  return (header ? [`sku${delimiter}quantity`, ...lines] : lines).join("\n");
}

const rows = [{ sku: "LAMP-01", quantity: 4 }];
exportStock(rows);
exportStock(rows, { delimiter: ";" });
exportStock(rows, { format: "json" });
export interface ExportOptions {
  format?: "csv" | "json";
  delimiter?: string;
  header?: boolean;
}

export function exportStock(
  rows: { sku: string; quantity: number }[],
  { format = "csv", delimiter = ",", header = true }: ExportOptions = {},
): string {
  if (format === "json") return JSON.stringify(rows);
  const lines = rows.map((row) => `${row.sku}${delimiter}${row.quantity}`);
  return (header ? [`sku${delimiter}quantity`, ...lines] : lines).join("\n");
}

const rows = [{ sku: "LAMP-01", quantity: 4 }];
exportStock(rows);
exportStock(rows, { delimiter: ";" });
exportStock(rows, { format: "json" });
An options object with defaults in the parameter list.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Every call passes some, all or none of the options, and inside the function format, delimiter and header have their full types with no undefined left in them.

Why it works

@property {string} [delimiter] is an optional property, like delimiter?: string. The destructuring pattern { format = "csv", delimiter = ",", header = true } = {} does two jobs: the = {} lets callers leave the whole object out, and each = value replaces a missing property. tsc follows both, so the variables it creates are "csv" | "json", string and boolean.

If you prefer the defaults in one object, merge them with spread syntax. Later properties win, so the caller’s options go last:

export.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 {object} ExportOptions
 * @property {"csv" | "json"} [format]
 * @property {string} [delimiter]
 * @property {boolean} [header]
 */

/** @type {Required<ExportOptions>} */
const DEFAULTS = { format: "csv", delimiter: ",", header: true };

/**
 * @param {ExportOptions} [options]
 * @returns {Required<ExportOptions>}
 */
export function resolveOptions(options = {}) {
  return { ...DEFAULTS, ...options };
}

const { delimiter } = resolveOptions({ delimiter: ";" });
console.log(delimiter.length);
The defaults as an object, merged with spread syntax.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

The same thing in TypeScript

The TypeScript equivalent tab of the first example uses an interface with format?: properties and puts the type after the pattern: { format = "csv", ... }: ExportOptions = {}. In JavaScript the type goes in the @param tag instead, and the pattern stays plain JavaScript.

What goes wrong

An object literal passed straight to the function is checked for extra properties, so a misspelled option is caught, with a suggestion (TS2561, a close relative of TS2353); a value outside the allowed strings is TS2322:

export.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: export.js(16,19): error TS2561: Object literal may only specify known properties, but 'delimeter' does not exist in type 'ExportOptions'. Did you mean to write 'delimiter'? export.js(18,19): error TS2322: Type '"xml"' is not assignable to type '"csv" | "json" | undefined'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. export.js(16,19): error TS2561: Object literal may only specify known properties, but 'delimeter' does not exist in type 'ExportOptions'. Did you mean to write 'delimiter'? export.js(18,19): error TS2322: Type '"xml"' is not assignable to type '"csv" | "json" | undefined'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. export.js(16,19): error TS2561: Object literal may only specify known properties, but 'delimeter' does not exist in type 'ExportOptions'. Did you mean to write 'delimiter'? export.js(18,19): error TS2322: Type '"xml"' is not assignable to type '"csv" | "json" | undefined'. checked 21 October 2026
/**
 * @typedef {object} ExportOptions
 * @property {"csv" | "json"} [format]
 * @property {string} [delimiter]
 */

/**
 * @param {{ sku: string }[]} rows
 * @param {ExportOptions} [options]
 */
export function exportStock(rows, { format = "csv", delimiter = "," } = {}) {
  return format === "json" ? JSON.stringify(rows) : rows.map((row) => row.sku).join(delimiter);
}

exportStock([], { delimeter: ";" });error TS2561: Object literal may only specify known properties, but 'delimeter' does not exist in type 'ExportOptions'. Did you mean to write 'delimiter'?
exportStock([], { format: "xml" });error TS2322: Type '"xml"' is not assignable to type '"csv" | "json" | undefined'.
A misspelled option and a value the type does not allow.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

The spread version has a gap. An optional property also accepts an explicit undefined, and { ...DEFAULTS, ...{ delimiter: undefined } } overwrites the default with undefined at runtime while the type still says string. With exactOptionalPropertyTypes on, tsc refuses that call:

export.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: export.js(15,16): error TS2379: Argument of type '{ delimiter: undefined; }' is not assignable to parameter of type 'ExportOptions' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'delimiter' are incompatible. Type 'undefined' is not assignable to type 'string'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. export.js(15,16): error TS2379: Argument of type '{ delimiter: undefined; }' is not assignable to parameter of type 'ExportOptions' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'delimiter' are incompatible. Type 'undefined' is not assignable to type 'string'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. export.js(15,16): error TS2379: Argument of type '{ delimiter: undefined; }' is not assignable to parameter of type 'ExportOptions' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. Types of property 'delimiter' are incompatible. Type 'undefined' is not assignable to type 'string'. checked 21 October 2026
/**
 * @typedef {object} ExportOptions
 * @property {string} [delimiter]
 */

/** @type {Required<ExportOptions>} */
const DEFAULTS = { delimiter: "," };

/** @param {ExportOptions} [options] */
export function resolveOptions(options = {}) {
  return { ...DEFAULTS, ...options };
}

resolveOptions({ delimiter: undefined });error TS2379: Argument of type '{ delimiter: undefined; }' is not assignable to parameter of type 'ExportOptions' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Types of property 'delimiter' are incompatible.
    Type 'undefined' is not assignable to type 'string'.
An explicit undefined slips past the spread until exactOptionalPropertyTypes is on.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": [],
    "exactOptionalPropertyTypes": true
  },
  "include": ["*.js"]
}

Gotchas

  • Only literals get the extra-property check. An options object built in a variable first is checked only for the keys it shares with the type. A misspelled key next to a correct one goes unreported; an object with no key in common is TS2559 (“has no properties in common”).
  • Spreads are shallow. Nested option objects are replaced, not merged; merge them one level down yourself.
  • Defaults replace undefined only. JavaScript keeps a null that a caller passes, so leave null out of the option types unless the function handles it.

Questions

Should the defaults live in the parameter list or in a DEFAULTS object?

Destructuring with defaults in the parameter list is the shortest and keeps every default next to its name. A DEFAULTS object merged with spread syntax suits options you also need elsewhere, such as in a --help text or a test.

Why does the type say [format] when the function always has a format?

The type describes what callers pass, and callers may leave format out. Inside the function the default fills it in, so the destructured format is never undefined.

How do I write the type of the options after the defaults are applied?

Required<ExportOptions> makes every property required. The DEFAULTS object and the merged result in the second example both have that type.

Sources

Last reviewed 21 October 2026.