Дженерики это параметры типа. Обобщённая функция объявляет тип-заполнитель, обычно с именем T, и каждый вызов его подставляет, так что входящие типы определяют выходящие. Одна функция тогда работает с числами, строками и вашими собственными объектами, и каждый вызов при этом полностью типизирован.
<T> после имени функции объявляет параметр типа. items: T[] использует его для входа, а T | undefined для выхода, и это связывает их. Вы ни разу не написали number или string: TypeScript вывел T из аргумента.
Почему не any или unknown?
Без дженериков параметр пришлось бы типизировать как any или unknown. Оба принимают любой массив, и оба теряют связь между входом и выходом:
| Тип параметра | Принимает любой вход | Тип результата | Проверяется |
|---|---|---|---|
any | да | any | нет: ошибки компилируются |
unknown | да | unknown | да, но перед использованием нужно сужать |
T (дженерик) | да | связан с входом | да, с точным типом |
Вывод аргументов типа
Обычно TypeScript выводит аргументы типа из переданных значений. Их также можно написать явно в угловых скобках при вызове; это нужно, когда T не упоминается ни в одном аргументе или когда вывод выбирает не то, что нужно.
Стоит знать два результата вывода. pair(1, "one") без явного аргумента даёт ошибку: TypeScript выводит T = number из первого аргумента и затем сообщает Argument of type 'string' is not assignable to parameter of type 'number'. (TS2345). А emptyList() вообще без аргумента получает T = unknown, то есть unknown[], что редко бывает нужно.
Несколько параметров типа
Функция может объявить столько параметров типа, сколько ей нужно, через запятую. Каждый выводится независимо.
mapValues выводит сразу три вещи: K это "tea" | "cake", V это number, а R это string из возвращаемого значения колбэка. K extends string это ограничение, о нём на следующей странице.
Обобщённые интерфейсы и псевдонимы типов
Типы тоже могут принимать параметры. Тогда аргумент пишется при использовании типа: Box<number>, ApiResponse<User>. Так устроено большинство встроенных типов коллекций: Array<T>, Map<K, V>, Promise<T>, Record<K, V>.
Result<T, E = string> показывает также параметр типа по умолчанию: Result<number> означает Result<number, string>. Как и опциональные параметры функций, параметры со значениями по умолчанию должны идти после обязательных.
Обобщённые классы
Класс принимает параметры типа после своего имени, и каждый экземпляр их фиксирует. Их могут использовать поля, методы и параметры конструктора.
Если конструктор принимает T, явный аргумент можно опустить: new Box(5) выводит Box<number>. Статические члены принадлежат самому классу, а не экземпляру, поэтому не могут использовать параметр типа класса (Static members cannot reference class type parameters., TS2302).
Обобщённые стрелочные функции и типы функций
Список параметров типа идёт перед списком параметров. Тот же синтаксис описывает обобщённый тип функции.
const last = <T>(items: T[]): T | undefined => items[items.length - 1];
type Mapper = <T, R>(items: T[], fn: (item: T) => R) => R[];
const mapAll: Mapper = (items, fn) => items.map(fn);
// In a .tsx file, <T> looks like a JSX tag. Add a trailing comma:
const lastTsx = <T,>(items: T[]) => items[items.length - 1];
Вывод литеральных типов через const-параметры типа
По умолчанию дженерик выводит расширенные типы: pair("a", "b") даёт [string, string]. Когда важны точные значения (имена маршрутов, имена событий, списки колонок), пометьте параметр как const (TypeScript 5.0 и новее), и он будет выводить тип так, будто аргумент записан с as const:
Вызывающий код пишет обычный массив, а функция сохраняет литеральные типы.
Когда дженерики не нужны
Параметр типа оправдан, когда связывает две вещи: параметр и возвращаемый тип, два параметра или параметр и колбэк. Если T встречается лишь один раз, он ничего не добавляет, и обычный тип понятнее.
// Pointless: T is used once, so it is just a longer way to write unknown
function logValue<T>(value: T): void {
console.log(value);
}
// Clearer
function logValueSimple(value: unknown): void {
console.log(value);
}
// Also pointless: returns T but nothing connects T to an argument,
// so the caller is really just asserting a type
function parseJson<T>(text: string): T {
return JSON.parse(text);
}
Последний вариант распространён и вводит в заблуждение: parseJson<User>(text) выглядит типобезопасным, но ничего не проверяет, ровно как JSON.parse(text) as User. Вместо этого возвращайте unknown и проверяйте. Другие признаки злоупотребления: параметр типа, которому всегда передаётся один и тот же аргумент, или параметр, который можно заменить объединением двух известных типов.
Для дженериков, которым нужно что-то знать о T (что у него есть length или что K это ключ T), следующий шаг это ограничения через extends.
Часто задаваемые вопросы
Что такое дженерики в TypeScript?
Дженерики это параметры типа: заполнители вроде T, которые подставляются реальным типом при каждом использовании функции, интерфейса, псевдонима типа или класса. function first<T>(items: T[]): T | undefined работает для любого массива, и результат имеет тип элемента переданного массива, поэтому first([1, 2]) это number | undefined, а first(["a"]) это string | undefined.
Чем дженерики отличаются от any?
any отключает проверку типов: между входящим и выходящим значением нет связи, и результат тоже any. Дженерик сохраняет связь: identity<T>(x: T): T возвращает ровно тот тип, который вы передали. Используйте unknown, если принимаете что угодно, но не должны возвращать тип обратно, и дженерик, если должны.
Что означает <T> в TypeScript?
Это объявление параметра типа с именем T. В function wrap<T>(value: T) T это переменная типа, которую TypeScript при каждом вызове заполняет по аргументу или которую вы передаёте явно как wrap<string>("a"). Имя T лишь соглашение; более длинные имена вроде TItem или Key работают так же.
Как написать обобщённую стрелочную функцию в TypeScript?
Поставьте список параметров типа перед параметрами: const first = <T>(items: T[]): T | undefined => items[0];. В файле .tsx <T> читалось бы как тег JSX, поэтому пишите <T,> с запятой в конце или <T extends unknown>.
Как задать тип по умолчанию для дженерика в TypeScript?
Добавьте = Type после параметра: interface ApiResponse<T = unknown> { data: T }. Тогда ApiResponse без аргумента означает ApiResponse<unknown>. Параметры со значениями по умолчанию должны идти после параметров без них, как опциональные параметры функций.