История вопроса:
В некоторых задачах требуется, чтобы функция могла возвращать разные структуры данных — например, массив фиксированной длины (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] }
Ключевые особенности:
Будет ли тип элемента массива после деструктуризации 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
Функция возвращает либо массив, либо ошибку, но не описывает это явно в type signature. В вызывающем коде ожидается, что результат — число, и обращение к result[1].toFixed(2) выбрасывает ошибку на runtime.
Плюсы:
Минусы:
Функция возвращает строго типизированный tuple с типом Result<T>, обработка результата строится на деструктуризации и явной runtime-проверке первого элемента (error/null). Компилятор гарантирует, что обращаться к результату можно только при успешном сужении типа.
Плюсы:
Минусы: