Type expression

Union types

  • Combining types
  • Since TypeScript 2.3

A union type is a list of types separated by |, and a value of that type can be any one of them. In JSDoc the union goes inside the braces exactly as you would write it in TypeScript: @param {string | number} id.

Until you narrow a union, TypeScript only lets you use what every member has in common. A typeof, instanceof, in or equality check narrows the value to the matching members inside that branch.

At a glance

JSDoc in a .js file TypeScript in a .ts file
/** @type {string | number} */ : string | number

Examples

format-id.js format-id.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
/**
 * @param {string | number} id
 * @returns {string}
 */
export function formatId(id) {
  if (typeof id === "number") {
    return `#${id.toString().padStart(6, "0")}`;
  }
  return id.toUpperCase();
}

formatId(42);
formatId("ab-12");
export function formatId(id: string | number): string {
  if (typeof id === "number") {
    return `#${id.toString().padStart(6, "0")}`;
  }
  return id.toUpperCase();
}

formatId(42);
formatId("ab-12");
A union parameter, narrowed with typeof.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
shout.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: shout.js(6,13): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. shout.js(6,13): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. shout.js(6,13): error TS2339: Property 'toUpperCase' does not exist on type 'string | number'. Property 'toUpperCase' does not exist on type 'number'. checked 21 October 2026
/**
 * @param {string | number} id
 */
export function shout(id) {
  return id.toUpperCase();error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.
  Property 'toUpperCase' does not exist on type 'number'.
}
Using a union without narrowing it.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}
size.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: size.js(13,10): error TS2345: Argument of type '"huge"' is not assignable to parameter of type 'Size'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. size.js(13,10): error TS2345: Argument of type '"huge"' is not assignable to parameter of type 'Size'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. size.js(13,10): error TS2345: Argument of type '"huge"' is not assignable to parameter of type 'Size'. checked 21 October 2026
/** @typedef {"small" | "medium" | "large"} Size */

/**
 * @param {Size} size
 * @returns {number}
 */
export function priceFor(size) {
  return size === "large" ? 12 : 9;
}

priceFor("medium");
priceFor("huge");error TS2345: Argument of type '"huge"' is not assignable to parameter of type 'Size'.
A union of string literals.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Writing a union

Separate the members with |: {string | number}, {"small" | "medium" | "large"}, {User | null}. The older JSDoc style with parentheses, {(string|number)}, means the same thing and TypeScript accepts it.

For a union you use more than once, give it a name with @typedef: /** @typedef {"small" | "medium" | "large"} Size */. The name works anywhere a type does, and other modules can use it through an import type.

Narrowing

Narrowing is ordinary JavaScript. The runtime checks you would write anyway are the ones TypeScript reads to work out which member you have:

  • typeof x === "string" for primitives
  • x instanceof Date for classes
  • Array.isArray(x) for arrays
  • "name" in x for objects with different properties
  • x === null or x != null for nullable values
  • a literal property every member has, such as kind: "circle", which makes a discriminated union

Unions of literals

A union of string literals is the usual JavaScript stand-in for an enum. The checker rejects any other string at the call site (TS2345 in the third example), and editors offer the allowed values as completions while you type the argument.

Gotchas

  • Calling a method that only one member has is an error until you narrow: toUpperCase exists on string but not on number, so id.toUpperCase() fails with TS2339.
  • {?string} and {string | null} mean the same thing in TypeScript. The second is clearer, and it is how the checker prints the type in its messages.
  • With strict (or strictNullChecks) off, null and undefined are part of every type, so writing {string | null} adds no protection.

Questions

How do I write an optional or nullable union in JSDoc?

Add null or undefined as a member: {string | null}. For an optional parameter, put its name in square brackets, @param {string} [label]; inside the function its type is then string | undefined.

Sources

Last reviewed 20 October 2026.