Типы возврата функций в TypeScript
Типизация возвращаемого значения — одна из ключевых возможностей TypeScript. Она позволяет зафиксировать контракт функции: что именно получит вызывающий код.
Правильный выбор типа возврата делает код предсказуемым и безопасным. Вызывающий код знает, что ожидать, а компилятор проверяет соответствие. В этой статье разберём все варианты типов возврата — от примитивов до дженериков и async-функций — и научимся выбирать подходящий тип для каждой задачи.
Содержание
- Конкретные типы
- void
- never
- any и unknown
- Union types
- Generics
- Асинхронные функции (Promise)
- Объекты и интерфейсы
- Массивы и кортежи
- Функции как возвращаемое значение
- Вывод типов (type inference)
- Таблица
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Конкретные типы
Самый простой случай — функция возвращает значение конкретного типа:
function getName(): string {
return "Alice";
}
function getAge(): number {
return 25;
}
function isActive(): boolean {
return true;
}
TypeScript проверяет, что return действительно возвращает значение указанного типа. Если нет — ошибка компиляции:
function getCount(): number {
return "five"; // ❌ Type 'string' is not assignable to type 'number'
}
void
void означает, что функция ничего не возвращает. Точнее, она завершается, но результат не имеет значения:
function log(message: string): void {
console.log(message);
}
function notify(user: string): void {
alert(`Hello, ${user}`);
}
Функция с void может технически содержать return, но без значения:
function doSomething(): void {
if (condition) {
return; // допустимо
}
// ...
}
Типичное применение — обработчики событий, мутации, побочные эффекты.
never
never — функция никогда не завершается нормально. Это не то же самое, что void:
function throwError(msg: string): never {
throw new Error(msg);
}
function infiniteLoop(): never {
while (true) {}
}
void — функция дошла до конца, но ничего не вернула.
never — функция не дошла до конца вообще (исключение, бесконечный цикл).
Exhaustive checks
Главное практическое применение never — проверка полноты switch:
type Status = "loading" | "success" | "error";
function render(status: Status) {
switch (status) {
case "loading": return "Загрузка...";
case "success": return "Данные загружены";
case "error": return "Ошибка";
default:
const _exhaustive: never = status;
return _exhaustive;
}
}
Если добавить "empty" в Status, TypeScript выдаст ошибку в default-ветке — status уже не never. Это страховка от забытых случаев.
any и unknown
Оба типа принимают любое значение, но разница принципиальна:
function parseAny(input: string): any {
return JSON.parse(input);
}
function parseUnknown(input: string): unknown {
return JSON.parse(input);
}
const a = parseAny("42");
a.toFixed(); // ✅ компилятор не ругается, но в рантайме может упасть
const b = parseUnknown("42");
b.toFixed(); // ❌ Error: Object is of type 'unknown'
any отключает проверку типов — это как вернуться в JavaScript.
unknown заставляет вызывающий код проверить тип перед использованием.
Правило: всегда предпочитайте unknown вместо any, если только нет веской причины.
Union types
Функция может возвращать одно из нескольких значений:
function findUser(id: number): User | null {
return users[id] ?? null;
}
function parse(input: string): string | number {
const n = Number(input);
return isNaN(n) ? input : n;
}
Вызывающий код обязан обработать все варианты через narrowing:
const result = parse("42");
if (typeof result === "string") {
result.toUpperCase(); // ✅ result: string
} else {
result.toFixed(); // ✅ result: number
}
Generics
Когда тип возврата зависит от типа аргумента, используются дженерики:
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b", "c"]); // s: string | undefined
Без дженерика пришлось бы использовать any или unknown, теряя информацию о типе:
function firstBad(arr: any[]): any {
return arr[0];
}
const x = firstBad([1, 2, 3]); // x: any — тип потерян
Дженерики позволяют сохранять связь между аргументами и возвращаемым значением.
Асинхронные функции (Promise)
async-функция всегда возвращает Promise<T>, где T — тип значения из return:
async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
async function findUser(id: number): Promise<User | null> {
const user = await fetchUser(id);
return user ?? null;
}
Тип Promise указывается явно. Внутри async-функции достаточно написать return value — TypeScript оборачивает его в Promise автоматически.
Объекты и интерфейсы
Функция может возвращать объект:
interface Point {
x: number;
y: number;
}
function createPoint(x: number, y: number): Point {
return { x, y };
}
Можно описать тип инлайн, без отдельного интерфейса:
function createUser(name: string): { name: string; id: number } {
return { name, id: Date.now() };
}
Для больших объектов предпочтительнее выносить тип в отдельный interface или type.
Массивы и кортежи
function getNames(): string[] {
return ["Alice", "Bob"];
}
function getPair(): [string, number] {
return ["Alice", 25];
}
Массив (string[]) — любое количество элементов одного типа.
Кортеж ([string, number]) — фиксированное количество элементов с конкретными типами.
Функции как возвращаемое значение
Функция может возвращать другую функцию:
function createMultiplier(factor: number): (value: number) => number {
return (value) => value * factor;
}
const double = createMultiplier(2);
double(5); // 10
Тип (value: number) => number описывает сигнатуру возвращаемой функции.
Вывод типов (type inference)
TypeScript часто может вывести тип возврата автоматически:
function add(a: number, b: number) {
return a + b; // TS сам выводит: number
}
function getUser() {
return { name: "Alice", age: 25 }; // TS выводит: { name: string; age: number }
}
Однако для публичных API рекомендуется указывать тип возврата явно:
// Хорошо — контракт зафиксирован
export function calculateTotal(items: CartItem[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Плохо — при изменении реализации может случайно измениться тип
export function calculateTotal(items: CartItem[]) {
return items.reduce((sum, item) => sum + item.price, 0);
}
Явный тип возврата — это документация и страховка от случайных изменений.
Таблица
| Тип | Когда использовать |
|---|---|
string, number, boolean, объекты | Функция возвращает конкретное значение |
void | Функция завершается, но не возвращает значение (побочные эффекты) |
never | Функция не завершается (throw, бесконечный цикл) + exhaustive checks |
unknown | Функция возвращает значение, тип которого неизвестен, но нужна проверка |
any | Избегать. Только для миграции с JS |
T | null, T | undefined | Функция может не найти результат |
T | U | Функция может вернуть разные типы |
Promise<T> | Асинхронная функция |
T[], [A, B] | Массив или кортеж |
(x: T) => U | Функция возвращает другую функцию |
Ключевые тезисы для интервью
- Конкретные типы (
string,number,boolean, объекты) — по умолчанию для всех функций. voidозначает, что функция завершается, но результат не имеет значения — для побочных эффектов.never— функция никогда не завершается нормально (throw, бесконечный цикл); также используется для exhaustive checks.unknownбезопаснееany: заставляет вызывающий код проверить тип перед использованием.- Union types (
T | U) требуют narrowing черезif,switchили type guards. - Дженерики связывают тип аргумента с типом возврата, сохраняя типобезопасность.
async-функция всегда возвращаетPromise<T>, гдеT— тип значения изreturn.- Для больших объектов выносите тип в отдельный
interfaceилиtype. - Массив (
T[]) — любое количество элементов; кортеж ([A, B]) — фиксированное количество с конкретными типами. - Явный тип возврата в публичных API — это документация и страховка от случайных изменений реализации.
Заключение
Правильный выбор типа возврата — основа предсказуемого кода. Конкретные типы для данных, void для побочных эффектов, never для неиспользуемых веток и exhaustive checks, unknown вместо any для неизвестных данных, дженерики для связи входа и выхода, Promise<T> для асинхронных функций. В публичных API указывайте тип возврата явно — это контракт, который защищает от случайных изменений.