ПрограммированиеFrontend разработчик

Как реализовать и типизировать функцию, возвращающую union type или tuple в зависимости от параметров? Как корректно описывать возвращаемые значения, чтобы не терять строгую типизацию при дальнейших операциях и использовании деструктуризации?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса:

В некоторых задачах требуется, чтобы функция могла возвращать разные структуры данных — например, массив фиксированной длины (tuple) или несколько вариантов результата (union type). В динамическом JavaScript этот подход широко используется для паттернов «результат или ошибка» (например, [err, value]), а в TypeScript требует чёткой типизации, чтобы последующий код правильно понимал структуру результата.

Проблема:

Без явного задания возвращаемого типа TypeScript приводит результат к слишком «широкой» (т.е. неузкой) форме — массив или union, что лишает разработчика преимуществ автодополнения и безопасности типов. Если деструктурировать результат функции, есть риск потерять точное описание типов элементов.

Решение:

Необходимо использовать conditional types и overload-описания функций, а для tuple — явно задавать тип результирующего массива и использовать as const (если возвращаем константный кортеж). Для union-результатов можно комбинировать generics и type narrowing, чтобы снаружи кода можно было корректно различать тип возвращаемого результата.

Пример кода:

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)); // С tuple и as const для auto-произвольных структур: 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] }

Ключевые особенности:

  • Используйте перегрузки (overloads) для функций, возвращающих различные типы в зависимости от параметров.
  • Для tuple всегда используйте [T, U] или as const, чтобы сохранить точную длину и тип элементов после деструктуризации.
  • Union типов возвращаемых значений требует type guards для узкого определения типа при дальнейшей работе.

Вопросы с подвохом.

Будет ли тип элемента массива после деструктуризации union возвращаемого tuple всегда конкретным?

Нет, если функция возвращает union кортежей, после деструктуризации элементы будут представлять объединение типов всех возможных вариаций.

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

Можно ли с помощью as const полностью "зафиксировать" тип tuple без дополнительного описания типа функции?

Нет, as const фиксирует значения, но если функция объявлена без возвратного типа, TypeScript может вывести тип шире, чем нужен. Лучше явно указывать return type.

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

Дают ли overloads с разными возвращаемыми типами гарантированное определение типа результата после деструктуризации?

Перегрузки помогают компилятору, но если входной параметр неизвестен, результат будет union из всех вариантов. Для точного сужения типа нужно использовать type guards в обработчике результата.

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

Типовые ошибки и анти-паттерны

  • Описание функции как возвращающей any[] или (T | U)[], что приводит к "размыванию" специфики tuple.
  • Попытка использовать деструктуризацию без type narrowing и проверки корректности типа элементов.
  • Отсутствие прямого описания структуры возвращаемого значения — усложнение поддержки и автодополнения.

Пример из жизни

Негативный кейс

Функция возвращает либо массив, либо ошибку, но не описывает это явно в type signature. В вызывающем коде ожидается, что результат — число, и обращение к result[1].toFixed(2) выбрасывает ошибку на runtime.

Плюсы:

  • Простой к написанию код без типизации.

Минусы:

  • Ошибка только в рантайме.
  • Потеря строгой типизации, больше багов.

Позитивный кейс

Функция возвращает строго типизированный tuple с типом Result<T>, обработка результата строится на деструктуризации и явной runtime-проверке первого элемента (error/null). Компилятор гарантирует, что обращаться к результату можно только при успешном сужении типа.

Плюсы:

  • Предсказуемость и ясность кода.
  • Автоматическое автодополнение для tuple.

Минусы:

  • Необходимость описывать типы вручную.
  • Увеличивается количество type guard и проверок.