ProgrammingFrontend Developer

How to implement and type a function that returns a union type or tuple depending on parameters? How to correctly describe the return values to maintain strict typing during further operations and use of destructuring?

Pass interviews with Hintsage AI assistant

Answer.

Background:

In some tasks, it is required that a function can return different data structures — for example, a fixed-length array (tuple) or multiple variants of a result (union type). In dynamic JavaScript, this approach is widely used for "result or error" patterns (e.g., [err, value]), while in TypeScript it requires clear typing so that subsequent code can properly understand the structure of the result.

Problem:

Without explicitly defining the return type, TypeScript leads the result to too "broad" (i.e., not narrow) a form — an array or union, which deprives the developer of the advantages of autocompletion and type safety. If you destructure the function's result, there is a risk of losing the precise description of the element types.

Solution:

It is necessary to use conditional types and overload function descriptions, and for tuples — explicitly define the resulting array type and use as const (if returning a constant tuple). For union results, you can combine generics and type narrowing to properly distinguish the return type externally.

Code example:

type Result<T> = [null, T] | [Error, null]; function parseNumber(str: string): Result<number> { const n = Number(str); return isNaN(n) ? [new Error('Invalid number'), null] : [null, n]; } const [err, value] = parseNumber('123'); if (err) console.error(err.message); else console.log(value!.toFixed(2)); // With tuple and as const for auto arbitrary structures: function getStatus(flag: boolean): [string, number] | string { return flag ? ['ok', 200] as const : 'error'; } const r = getStatus(true); if (Array.isArray(r)) { // r: readonly [string, number] }

Key features:

  • Use overloads for functions that return different types depending on parameters.
  • Always use [T, U] or as const for tuples to preserve the exact length and type of elements after destructuring.
  • Union types of return values require type guards for narrow type definition during further processing.

Trick questions.

Will the type of the array element after destructuring the union returned tuple always be specific?

No, if the function returns a union of tuples, after destructuring the elements will represent a union of types of all possible variations.

type R = [number, null] | [null, string]; const [a, b]: R = [1, null]; // a: number | null // b: null | string

Can as const completely "fix" the tuple type without additional function type description?

No, as const fixes the values, but if the function is declared without a return type, TypeScript may infer the type wider than necessary. It's better to explicitly specify the return type.

function foo() { return [1, 'ok'] as const; } // foo(): readonly [1, "ok"]

Do overloads with different return types guarantee a definite type of the result after destructuring?

Overloads help the compiler, but if the input parameter is unknown, the result will be a union of all variants. To precisely narrow the type, type guards must be used in the result handler.

function bar(x: number): string; function bar(x: boolean): number; function bar(x: any): any { return typeof x === 'number' ? 'str' : 123; } const r = bar(Math.random() > 0.5 ? true : 1); // r: number | string

Typing errors and anti-patterns

  • Describing a function as returning any[] or (T | U)[], which leads to the "blurring" of tuple specificity.
  • Attempting to use destructuring without type narrowing and checking the correctness of the element types.
  • Lack of a direct description of the structure of the return value — complicating support and autocompletion.

Real-life example

Negative case

A function returns either an array or an error, but does not explicitly describe this in the type signature. In the calling code, it is expected that the result is a number, and accessing result[1].toFixed(2) throws an error at runtime.

Pros:

  • Easy to write code without typing.

Cons:

  • Error only at runtime.
  • Loss of strict typing, more bugs.

Positive case

A function returns a strictly typed tuple with the type Result<T>, the result processing is based on destructuring and explicit runtime checking of the first element (error/null). The compiler guarantees that you can only access the result upon successful type narrowing.

Pros:

  • Predictability and clarity of code.
  • Automatic autocompletion for tuples.

Cons:

  • Need to describe types manually.
  • Increased amount of type guards and checks.