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
/**
* @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");jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /**
* @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'.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} /** @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'.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 primitivesx instanceof Datefor classesArray.isArray(x)for arrays"name" in xfor objects with different propertiesx === nullorx != nullfor 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:
toUpperCaseexists onstringbut not onnumber, soid.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(orstrictNullChecks) off,nullandundefinedare 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
- Everyday Types: Union Types · TypeScript handbook
- Narrowing · TypeScript handbook
- JSDoc Reference · TypeScript handbook
Last reviewed 20 October 2026.