Menu

Дженерики в TypeScript: обобщённые функции, типы и классы

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

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Дженерики это параметры типа. Обобщённая функция объявляет тип-заполнитель, обычно с именем 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>. Параметры со значениями по умолчанию должны идти после параметров без них, как опциональные параметры функций.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ