Recipe · Objects and data

How do I return a tuple from a JavaScript function?

  • Node.js (ES modules)
  • Tuples
  • @returns
  • Const assertions
  • Checked with TypeScript 5.9

The problem My function returns [found, quantity], and after destructuring both variables are boolean | number. How do I tell tsc that the first element is a boolean and the second a number?

The short answer

Write the tuple type in @returns, optionally with a label for each position:

stock.js stock.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 {Map<string, number>} stock
 * @param {string} sku
 * @returns {[inStock: boolean, quantity: number]}
 */
export function checkStock(stock, sku) {
  const quantity = stock.get(sku) ?? 0;
  return [quantity > 0, quantity];
}

const [inStock, quantity] = checkStock(new Map([["LAMP-01", 4]]), "LAMP-01");
if (inStock) console.log(quantity.toFixed(0));
export function checkStock(stock: Map<string, number>, sku: string): [inStock: boolean, quantity: number] {
  const quantity = stock.get(sku) ?? 0;
  return [quantity > 0, quantity];
}

const [inStock, quantity] = checkStock(new Map([["LAMP-01", 4]]), "LAMP-01");
if (inStock) console.log(quantity.toFixed(0));
A function that returns a labeled tuple.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

After destructuring, inStock is a boolean and quantity a number.

Why it works

An array literal like [quantity > 0, quantity] could be a tuple or a list, and without a hint TypeScript infers the more general one: an array whose elements are any of the values, (number | boolean)[]. The @returns type tells tsc the function returns a pair, and the literal is checked against it position by position. Labels such as inStock: arrived in TypeScript 4.0 and only name the positions.

A const cast gets the same result without writing the type, and makes the tuple read-only:

stock.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
/**
 * @param {Map<string, number>} stock
 * @param {string} sku
 */
export function checkStock(stock, sku) {
  const quantity = stock.get(sku) ?? 0;
  return /** @type {const} */ ([quantity > 0, quantity]);
}

const [inStock, quantity] = checkStock(new Map([["LAMP-01", 4]]), "LAMP-01");
if (inStock) console.log(quantity.toFixed(0));
A const cast keeps the tuple without a @returns.
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 writes the same tuple as the return type, : [inStock: boolean, quantity: number]. The const-cast version would be return [quantity > 0, quantity] as const;.

What goes wrong

Without either, the return type is an array, both destructured variables are number | boolean, and number methods fail (TS2339):

stock.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: stock.js(12,35): error TS2339: Property 'toFixed' does not exist on type 'number | boolean'. Property 'toFixed' does not exist on type 'false'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. stock.js(12,35): error TS2339: Property 'toFixed' does not exist on type 'number | boolean'. Property 'toFixed' does not exist on type 'false'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. stock.js(12,35): error TS2339: Property 'toFixed' does not exist on type 'number | boolean'. Property 'toFixed' does not exist on type 'false'. checked 21 October 2026
/**
 * @param {Map<string, number>} stock
 * @param {string} sku
 */
export function checkStock(stock, sku) {
  const quantity = stock.get(sku) ?? 0;
  return [quantity > 0, quantity];
}

const [inStock, quantity] = checkStock(new Map([["LAMP-01", 4]]), "LAMP-01");
if (inStock) console.log(quantity.toFixed(0));error TS2339: Property 'toFixed' does not exist on type 'number | boolean'.
  Property 'toFixed' does not exist on type 'false'.
Without @returns, the array is inferred as an array.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • A const cast makes the tuple readonly. Callers cannot push to it or assign to its positions; that is usually what you want for a return value.
  • The parentheses matter. /** @type {const} */ ([a, b]) is a cast; without the parentheses around the expression the comment is ignored.
  • Optional positions use ?. [sku: string, quantity?: number] describes a tuple whose second element may be missing.

Questions

What do the labels in [inStock: boolean, quantity: number] do?

They name the positions for readers and for editor hints; they do not change the type. A tuple without labels, [boolean, number], accepts the same values.

Should I return an object instead?

For two or three values that callers always destructure, a tuple is fine (React's useState is the familiar example). For more, or for values callers pick from, an object with named properties is easier to read and to extend.

Sources

Last reviewed 21 October 2026.