The short answer
Write the tuple type in @returns, optionally with a label for each position:
/**
* @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));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:
/**
* @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));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):
/**
* @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'.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 cannotpushto 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
- Object Types (tuple types) · TypeScript handbook
- TypeScript 4.0 release notes (labeled tuple elements) · TypeScript handbook
- Destructuring assignment · MDN Web Docs
Last reviewed 21 October 2026.