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

Как работает тип ReturnType<T> в TypeScript, чем он отличается от ручного вывода типа возвращаемого значения функции, и какие риски/выгоды есть при его использовании?

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

Ответ.

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

С развитием TypeScript появилась необходимость автоматически извлекать тип возвращаемого значения из функции, особенно для больших проектов с множеством связанных между собой функций. Для этого был введён утилитарный тип ReturnType<T>, который появился в стандартной библиотеке начиная с версии TypeScript 2.8.

Проблема

В больших и сложных проектах бывает сложно поддерживать актуальность типов — если вручную указывать тип возвращаемого значения каждой функции и при этом произвести изменение сигнатур, легко получить рассогласованность, когда сигнатуры и реализации не совпадают. Дополнительно, если функция возвращает сложную структуру, не всегда легко вручную синхронизировать описание возвращаемого типа во всех местах использования.

Решение

Утилитарный тип ReturnType<T> автоматически извлекает возвращаемый тип функций и может использоваться для типизации результата вызова функции в любом месте кода. Это снижает количество ручной поддержки типовой инфраструктуры и минимизирует ошибки, связанные с несоответствием описанного и фактического типа возвращаемого значения.

Пример кода:

function createUser(name: string, age: number) { return { name, age, created: new Date() }; } type User = ReturnType<typeof createUser>; // User: { name: string; age: number; created: Date; }

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

  • Автоматически извлекает тип возвращаемого значения функций (в том числе и generics), избавляя от дублирования кода.
  • Снижает вероятность ошибки при изменениях сигнатуры функции или структуры возвращаемого значения.
  • Не работает с перегрузками функций — извлекает только общий (широкий) возвращаемый тип.

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

Можно ли использовать ReturnType с методами классов?

Да, но важно помнить о контексте: если метод — это свойство объекта с функцией, используйте ReturnType<obj['метод']>.

Пример кода:

class MyClass { foo(x: number) { return x * 2; } } type FooReturn = ReturnType<MyClass['foo']>; // Type error! // Нужно так: type FooReturn = ReturnType<(x: number) => number>; // number // Или вынести метод как функцию: const obj = new MyClass(); type FooReturn2 = ReturnType<typeof obj.foo>;

Что вернет ReturnType для функций с void/never?

Для функции с объявленным типом void, ReturnType вернет void. Для never — never.

Пример кода:

function doNothing(): void {} type Result = ReturnType<typeof doNothing>; // void

Работает ли ReturnType с перегрузками функций?

Нет, ReturnType извлекает возвращаемый тип самой "реализации", а не всех перегрузок. Если перегрузок несколько — берется описанный тип реализации.

Пример кода:

function func(x: number): number; function func(x: string): string; function func(x: any): any { return x } type RT = ReturnType<typeof func>; // any

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

  • Использование ReturnType с перегруженными функциями приводит к неожиданным типам.
  • Забвение, что ReturnType не вычисляет return-тип значения Promise — нужен Awaited или ручная работа.
  • Полное полагание на ReturnType при изменении логики функции без обновления других зависимых частей кода.

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

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

В проекте объявляют ручной тип для возвращаемого объекта функции. Функция меняется — тип не обновляется, вызовы падают в рантайме.

Плюсы:

  • Типы легко читать, можно регулировать их вручную.

Минусы:

  • Быстро устаревают, появляется "дрейф" между типами и фактическим API.

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

Переходят на ReturnType везде, где используют значение, возвращаемое функцией. В случае изменений тип всегда актуален.

Плюсы:

  • Минимальное дублирование, типизированное соответствие фактической реализации.

Минусы:

  • Могут возникнуть сложности с пониманием типовой магии у новичков.