How to return tuple in typescript

Tuesday 02/08/2022

·5 min read
Share:

TypeScript 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 const makes nested objects readonly too. return [{ x: 10 }] as const gives you readonly [{ readonly x: 10 }]. Sometimes you want this, sometimes you don't. For partial readonly, build the tuple manually.
  • Function overloads can express variadic tuples (...rest patterns), 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()) returns true for 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.

Share:
VA

Vadim Alakhverdov

Software developer writing about JavaScript, web development, and developer tools.

Related Posts