How to return tuple in typescript
Tuesday 02/08/2022
·5 min readTypeScript doesn't have a dedicated tuple syntax for return values, so you have to nudge the type system in one of three ways. By default, return [a, b] is inferred as (typeof a | typeof b)[] — a union array, not a tuple. That's almost never what you want. The fix is either as const, an explicit tuple type annotation, or named tuples (TypeScript 4.0+) for self-documenting return shapes. Below: all three, with the cases each is best for.
The default behavior — and why it's wrong
Without any annotation, TypeScript infers the most general type that fits:
function getCoords() {
return [10, 20]
}
const coords = getCoords()
// coords: number[]
const [x, y] = coords
// x: number, y: number ✓ works
const [a, b, c, d] = coords
// a, b, c, d: number ✗ also "works" — no error on extra destructuring
The compiler sees number[], so destructuring as many elements as you want is allowed. That's loose typing dressed up as inference. For a tuple — a fixed-length, position-typed sequence — you need stronger constraints.
1. as const — the quickest fix
as const (a const assertion) freezes the array literal into its narrowest possible type:
function getCoords() {
return [10, 20] as const
}
const coords = getCoords()
// coords: readonly [10, 20]
The return type is now a readonly tuple with literal 10 and 20. That's often too narrow — usually you want number, not the specific literal 10. To widen while keeping the tuple shape:
function getCoords(x: number, y: number) {
return [x, y] as const
}
// returns: readonly [number, number]
as const also makes the tuple readonly — destructuring still works, but trying to coords[0] = 99 is a compile error. Use this when you want immutability anyway.
2. Explicit tuple type annotation
If you don't want readonly, annotate the return type explicitly:
function getCoords(): [number, number] {
return [10, 20]
}
const coords = getCoords()
// coords: [number, number]
coords[0] = 99 // ✓ allowed
This is the right choice when the function is part of a public API and you want the type annotation visible at the signature. It's also the only way to express a mutable tuple return.
3. Named tuples — self-documenting returns
TypeScript 4.0 added named tuple elements. The names are documentation-only (they don't affect runtime or destructuring), but they show up in editor tooltips and improve readability:
function getCoords(): [x: number, y: number] {
return [10, 20]
}
// Hover shows: getCoords(): [x: number, y: number]
const [a, b] = getCoords()
// destructured names don't need to match
This is genuinely useful when returning more than two elements — [startTime: number, endTime: number, errorCount: number] is far clearer than [number, number, number].
The React hook pattern
The most common reason to return tuples in TypeScript is React-style hooks:
function useToggle(initial = false) {
const [value, setValue] = useState(initial)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle] as const
}
// returns: readonly [boolean, () => void]
// usage
const [isOpen, toggleOpen] = useToggle()
as const is the right tool here because hooks conventionally return readonly pairs (state, action) — the caller shouldn't be mutating the array. The library convention is so strong that ESLint plugins flag mutable hook returns.
Edge cases worth knowing
as constmakes nested objects readonly too.return [{ x: 10 }] as constgives youreadonly [{ readonly x: 10 }]. Sometimes you want this, sometimes you don't. For partial readonly, build the tuple manually.- Function overloads can express variadic tuples (
...restpatterns), but the standard return-tuple needs none of that complexity. - Tuple types are still arrays at runtime. TypeScript erases the type info —
Array.isArray(getCoords())returnstruefor all three patterns. Don't rely on the type to enforce anything at runtime. - Destructuring with defaults works:
const [x = 0, y = 0] = getCoords()— useful for optional tuple elements typed as[number?, number?].
Picking the right approach
| You want | Use |
|---|---|
| Readonly tuple, narrow types | return [...] as const |
| Mutable tuple, explicit signature | : [T, T] annotation |
| Self-documenting return | named tuples : [x: T, y: T] |
| React hook return | as const (convention) |
FAQ
Why does TypeScript infer number[] instead of a tuple by default?
TypeScript prefers the most general type that fits. [10, 20] could grow to any length at runtime, so the inferred type is the more flexible number[]. To get a tuple, you have to explicitly opt in via as const or an explicit annotation.
Is as const better than an explicit tuple type?
as const is more concise and produces a readonly tuple, which is often what you want for return values. Use explicit annotations when the function is a public API and you want the tuple shape visible at the signature, or when you specifically need a mutable tuple.
Can I name elements in a TypeScript tuple?
Yes, since TypeScript 4.0: : [x: number, y: number]. The names are documentation-only — they show in IDE tooltips but don't constrain destructuring names. Useful when a tuple has more than two elements.
What's the difference between a tuple and a fixed-length array in TypeScript?
A tuple has per-position types ([string, number] has a string at index 0 and a number at index 1). A fixed-length array of one type is written as string[] & { length: 3 } or with a custom utility — but a tuple [string, string, string] is usually what people actually want.