Recipe · Functions

How do I write function overloads in JavaScript?

  • Node.js (ES modules)
  • @overload
  • Overloads
  • TypeScript 5.0
  • Checked with TypeScript 5.9

The problem My function returns one product for a string and a list for an array. With a union return type every caller has to narrow the result. How do I give each kind of call its own return type in JavaScript?

The short answer

Write one JSDoc comment per call signature, each starting with @overload, above the function. The comment right above the function types the implementation:

products.js products.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 {{ sku: string, name: string, price: number }} Product */

/** @type {Map<string, Product>} */
const catalog = new Map([["LAMP-01", { sku: "LAMP-01", name: "Desk lamp", price: 3900 }]]);

/**
 * @overload
 * @param {string} sku
 * @returns {Product | undefined}
 */
/**
 * @overload
 * @param {string[]} skus
 * @returns {Product[]}
 */
/**
 * @param {string | string[]} skuOrSkus
 * @returns {Product | undefined | Product[]}
 */
export function findProducts(skuOrSkus) {
  if (typeof skuOrSkus === "string") return catalog.get(skuOrSkus);
  return skuOrSkus.flatMap((sku) => catalog.get(sku) ?? []);
}

const lamp = findProducts("LAMP-01");
const basket = findProducts(["LAMP-01", "DESK-02"]);
console.log(lamp?.name, basket.length);
type Product = { sku: string; name: string; price: number };

const catalog = new Map<string, Product>([["LAMP-01", { sku: "LAMP-01", name: "Desk lamp", price: 3900 }]]);

export function findProducts(sku: string): Product | undefined;
export function findProducts(skus: string[]): Product[];
export function findProducts(skuOrSkus: string | string[]): Product | undefined | Product[] {
  if (typeof skuOrSkus === "string") return catalog.get(skuOrSkus);
  return skuOrSkus.flatMap((sku) => catalog.get(sku) ?? []);
}

const lamp = findProducts("LAMP-01");
const basket = findProducts(["LAMP-01", "DESK-02"]);
console.log(lamp?.name, basket.length);
Two call signatures with @overload, one implementation.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

findProducts("LAMP-01") now returns Product | undefined and findProducts([...]) returns Product[], so basket.length needs no check.

Why it works

TypeScript 5.0 added @overload to the JSDoc it reads. Each @overload comment becomes one call signature, exactly like the bodiless function findProducts(...): ...; lines in a .ts file. When you call the function, tsc tries the signatures in order and uses the first one the arguments fit; the return type comes from that signature.

The implementation’s own JSDoc (string | string[] in, the union out) must be broad enough to cover every overload, and callers never see it.

The same thing in TypeScript

The TypeScript equivalent tab has the classic form: two declarations without bodies, then the implementation. The JavaScript version is the same three signatures, written as comments.

What goes wrong

A call that fits no signature reports every overload it tried (TS2769):

products.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: products.js(22,14): error TS2769: No overload matches this call. Overload 1 of 2, '(sku: string): Product | undefined', gave the following error. Argument of type 'number' is not assignable to parameter of type 'string'. Overload 2 of 2, '(skus: string[]): Product[]', gave the following error. Argument of type 'number' is not assignable to parameter of type 'string[]'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. products.js(22,14): error TS2769: No overload matches this call. Overload 1 of 2, '(sku: string): Product | undefined', gave the following error. Argument of type 'number' is not assignable to parameter of type 'string'. Overload 2 of 2, '(skus: string[]): Product[]', gave the following error. Argument of type 'number' is not assignable to parameter of type 'string[]'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. products.js(22,14): error TS2769: No overload matches this call. The last overload gave the following error. Argument of type 'number' is not assignable to parameter of type 'string[]'. checked 21 October 2026
/** @typedef {{ sku: string, name: string }} Product */

/**
 * @overload
 * @param {string} sku
 * @returns {Product | undefined}
 */
/**
 * @overload
 * @param {string[]} skus
 * @returns {Product[]}
 */
/**
 * @param {string | string[]} skuOrSkus
 * @returns {Product | undefined | Product[]}
 */
export function findProducts(skuOrSkus) {
  return typeof skuOrSkus === "string" ? undefined : [];
}

findProducts(1001);error TS2769: No overload matches this call.
  Overload 1 of 2, '(sku: string): Product | undefined', gave the following error.
    Argument of type 'number' is not assignable to parameter of type 'string'.
  Overload 2 of 2, '(skus: string[]): Product[]', gave the following error.
    Argument of type 'number' is not assignable to parameter of type 'string[]'.
A call that matches neither overload.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Without overloads, the function has one signature with a union return type, and every caller pays for it. The array call gets Product | undefined | Product[], so reading .length fails twice, once for undefined (TS18048) and once because a single Product has no length:

products.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: products.js(13,13): error TS18048: 'basket' is possibly 'undefined'. products.js(13,20): error TS2339: Property 'length' does not exist on type 'Product | Product[]'. Property 'length' does not exist on type 'Product'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. products.js(13,13): error TS18048: 'basket' is possibly 'undefined'. products.js(13,20): error TS2339: Property 'length' does not exist on type 'Product | Product[]'. Property 'length' does not exist on type 'Product'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. products.js(13,13): error TS18048: 'basket' is possibly 'undefined'. products.js(13,20): error TS2339: Property 'length' does not exist on type 'Product[] | Product'. Property 'length' does not exist on type 'Product'. checked 21 October 2026
/** @typedef {{ sku: string, name: string }} Product */

/**
 * @param {string | string[]} skuOrSkus
 * @returns {Product | undefined | Product[]}
 */
export function findProducts(skuOrSkus) {
  return typeof skuOrSkus === "string" ? undefined : [];
}

const basket = findProducts(["LAMP-01", "DESK-02"]);
console.log(basket.length);error TS18048: 'basket' is possibly 'undefined'.error TS2339: Property 'length' does not exist on type 'Product | Product[]'.
  Property 'length' does not exist on type 'Product'.
Without overloads, every call gets the whole union.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • Order matters. tsc picks the first overload that fits, so put the narrow signatures before the wide ones.
  • One comment per signature reads best. The release notes put each @overload in its own /** */ block, with its own @param and @returns. Several @overload tags in one comment work too, but are harder to scan.
  • A generic can be simpler. When the return type follows the argument’s type directly (an array in, an array of the same thing out), a @template function avoids listing every case.

Questions

Which TypeScript version supports @overload?

TypeScript 5.0 added the @overload tag for JavaScript files. Older versions ignore it, so a project on 4.x needs another approach, such as two functions with different names.

Is the last JSDoc comment an overload too?

No. The comment directly above the function, without @overload, types the implementation. Callers only see the @overload signatures; the implementation's own signature is hidden from them.

Sources

Last reviewed 21 October 2026.