Перейти к основному содержимому

Дженерики в TypeScript

Разработчику Архитектору

Дальше: Паттерны · Объекты и классы · Справочник — утилитарные типы

Теория: Обобщения и обобщённое программирование — определения, зачем нужны generics, отличия языков.


Дженерики (generics) — параметры типа: одна функция или класс работает с разными данными, компилятор сохраняет связь между аргументом и результатом. Без них пришлось бы дублировать код или опираться на any. Удобный порядок — функции, эта статья, async Promise<T>.

Аналогия в C# — обобщения. Указатель в JS-курсе — JS-курс: дженерики.


Связанный выбор — один аргумент сужает другой

В выразительном TypeScript часто работает одна схема — первый выбор (ключ, имя события, исходная единица, статус ответа) задаёт второй (тип значения, payload, целевая единица, доступные поля).

Первый выборВторой сужается доГде разобрано
ключ Kтип T[K]ниже §keyof, updater
имя события EPayloadMap[E]паттерны — event bus
единица fromтолько совместимые to§Compat
status в unionполя веткиnarrowing, Exclude

На уровне значений сужение делает if / switch (Операторы и условные ветвления в TypeScript); на уровне сигнатур — ограничения T extends … и вспомогательные типы вроде Compat<F>.


Обобщённые типы (generics)

Play ITЗагрузка интерактивного демо…

Без genericsС generics
function first(arr: any[])function first<T>(arr: T[]): T | undefined
Потеря типа элементаIDE знает точный тип результата
Дублирование firstString, firstNumberОдна реализация
function first<T>(items: T[]): T | undefined {
return items[0];
}

const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefined

Разбор:

  • Tпараметр типа, подставляется при вызове.
  • Компилятор выводит T из аргумента, явно писать <number> не обязательно.

Синтаксис — функции, типы, классы

Функция

function identity<T>(value: T): T {
return value;
}

const a = identity<string>("text");
const b = identity(42); // T = number

Тип и interface

type Box<T> = { value: T };

interface Repository<T extends { id: string }> {
findById(id: string): T | undefined;
save(entity: T): void;
}

Класс

class Container<T> {
constructor(private value: T) {}

getValue(): T {
return this.value;
}

setValue(value: T): void {
this.value = value;
}
}

const c = new Container("hello"); // Container<string>

Разбор:

  • Поле value имеет тип T на всём жизненном цикле экземпляра.
  • Методы наследуют тот же T.

Соглашения об именах

ИмяОбычно означает
TПроизвольный тип
KKey (ключ)
VValue (значение)
EElement
TResultТип результата

Имена произвольны; важна одинаковая буква в сигнатуре и теле.


Ограничения (extends)

Параметр типа можно ограничить минимальным контрактом:

interface HasId {
id: string;
}

function getId<T extends HasId>(entity: T): string {
return entity.id;
}
interface Lengthwise {
length: number;
}

function logLength<T extends Lengthwise>(value: T): void {
console.log(value.length);
}

logLength("abc"); // OK
logLength([1, 2, 3]); // OK
// logLength(42); // ошибка: у number нет length

Разбор:

  • T extends HasId означает: тип T обязан иметь поле id: string.
  • Внутри функции доступны только поля контракта HasId, если не сузить дальше.

Несколько параметров

function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}

keyof и индексированный доступ

type User = { id: string; name: string; role: "admin" | "user" };

type UserKey = keyof User; // "id" | "name" | "role"
type UserName = User["name"]; // string

function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}

const user: User = { id: "1", name: "Анна", role: "user" };
const name = getField(user, "name"); // string

Разбор:

  • K extends keyof T — ключ должен существовать в T.
  • Возврат T[K] — точный тип поля, без any.

Связанные generic — выбор по группе (Compat)

Иногда второй generic-параметр зависит от первого по общей группе (размерность, категория, домен), без привязки к ключу объекта. Пример — конвертер единиц: из km можно перевести только в длину, в массу — нельзя.

Код ITЗагрузка примера кода…

Разбор:

  • as const сохраняет литеральные ключи и поля; satisfies Record<string, UnitDef> проверяет форму без расширения типов — 10.md §as const.
  • Compat<F>mapped type + условный тип — для каждого ключа P оставляем P, если base совпадает с base у F, иначе never; индексация [UnitKey] собирает union совместимых единиц.
  • T extends Compat<F>связанный (dependent) generic: после выбора from автодополнение для to показывает только ту же группу.

Проверка на уровне типов (без runtime):

type MiOk = "mi" extends Compat<"km"> ? true : false; // true
type KgBad = "kg" extends Compat<"km"> ? true : false; // false

Тот же приём — роли в одном домене, валюты, типы узлов в AST: общее поле-группа + Compat-подобный тип.


Паттерн — type-safe updater

Связка Omit, keyof и generic — из практики крупных кодовых баз:

Код ITЗагрузка примера кода…

Смысл ограничения: нельзя обновить запрещённое поле и нельзя подставить значение неверного типа.

Подробнее — Справочник — комбо-паттерн обновления.


Generic-класс — репозиторий

Код ITЗагрузка примера кода…


Дженерики в стандартной библиотеке

APIПример
Array<T>string[]
Promise<T>Promise<User>Асинхронность
Map<K, V>Map<string, number>
Record<K, V>Record<Role, string[]>

Код ITЗагрузка примера кода…

Разбор:

  • Promise<User> — тип всей async-функции; await res.json() даёт unknown, без гарантии User.
  • value is User — type predicate: после if (!isUser(raw)) в ветке return raw тип уже User.
  • as Promise<User> на json() — частая ошибка: json() возвращает Promise<unknown>, без типизированного ответа API.

Значения по умолчанию для параметра типа

type ApiResponse<TData = unknown> = {
ok: boolean;
data: TData;
};

type UserResponse = ApiResponse<User>;
type EmptyResponse = ApiResponse; // data: unknown

Обязательные generic-параметры идут перед параметрами с default.


Условные типы и infer

Продвинутый уровень — выбор типа по условию:

type IsString<T> = T extends string ? true : false;

type ReturnTypeOf<T> = T extends (...args: never[]) => infer R ? R : never;

type Fn = () => string;
type R = ReturnTypeOf<Fn>; // string

Разбор:

  • infer R извлекает тип возвращаемого значения из сигнатуры функции.
  • Встроенный ReturnType<T> делает то же — функции §утилиты.

Mapped types ({ [K in keyof T]: … }) — ниже и в справочнике §8.


Шаблонные строковые типы (Template Literal Types)

Template literal type — тип строки, собранной по шаблону (как `hello-${Name}`), но на уровне типов. Позволяет строить union имён, путей, CSS-классов и ключей API из других типов.

type Axis = "x" | "y";
type EventName = "click" | "focus";
type HandlerName = `on${Capitalize<EventName>}`; // "onClick" | "onFocus"

type CssVar = `--color-${"primary" | "danger"}`; // "--color-primary" | "--color-danger"

type Getter = `get${Capitalize<Axis>}`; // "getX" | "getY"

Встроенные утилиты для манипуляции строками на уровне типов:

UtilityНазначение
Uppercase<S>верхний регистр
Lowercase<S>нижний регистр
Capitalize<S>первая буква заглавная
Uncapitalize<S>первая буква строчная
type HttpMethod = "GET" | "POST";
type Route = "/users" | "/orders";
type Endpoint = `${HttpMethod} ${Route}`;
// "GET /users" | "GET /orders" | "POST /users" | "POST /orders"

Разбор:

  • Шаблон `${A}-${B}` строит декартово произведение union — осторожно с большими множествами.
  • Часто сочетают с as const объектом и keyof typeofТипы §as const.
  • Практический пример путей во вложенном объекте — §Path и PathType.

Сопоставленные типы и обобщённая индексация

Сопоставленный тип (mapped type) строит новый объектный тип по шаблону для каждого ключа. Обобщённая индексация — запись PayloadMap[E]: по имени события E компилятор подставляет точную форму payload.

Форма payload привязана к имени события. Обратились к полю из другого события внутри обработчика — получите ошибку компиляции вместо сюрприза в runtime.

Код ITЗагрузка примера кода…

Разбор:

  • as const на WebhookEvent сохраняет литеральные строки; WebhookEventType выводится без ручного union — 10.md §as const.
  • PayloadMap — mapped type: ключ = имя события, значение = контракт payload.
  • WebhookHandler<E> + PayloadMap[E]generic indexing: параметр типа E выбирает ветку карты.
  • Тот же приём в event bus (Events[K]) и в связанном выборе.

Отличие от класса TypedEmitter: здесь тип описывает отдельные функции-обработчики; шина добавляет регистрацию и emit в runtime.


Пути во вложенном объекте — Path и PathType

Для конфигов, форм и state иногда нужны строковые пути ("user.preferences.theme") с точным типом значения на конце. Два вспомогательных типа строят union путей и извлекают тип по пути.

Код ITЗагрузка примера кода…

Разбор:

  • Path<T>рекурсия + template literal types (`${K}.${…}`): обход вложенных объектов на уровне типов.
  • PathTypeinfer Key / infer Rest: разбор строки пути по первой точке и рекурсивный спуск.
  • В продакшене часто берут готовые библиотеки; ниже — упрощённая "чистая" версия для учёбы.

Ограничения упрощённой версии: optional-поля, массивы и union внутри объекта усложняют Path; для учебного примера достаточно вложенных обязательных полей, как выше.


Собственный Pick

type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};

type UserPreview = MyPick<User, "id" | "name">;

В проектах используйте встроенный Pick / Omitтипы §utility.


Частые ошибки

ОшибкаЧто делать
Слишком общий T без extendsДобавить минимальный контракт
Generic без реальных кейсовУпростить до конкретного типа
<T,> с запятойВ .tsx запятая отличает generic от JSX
Путать T[] и Array<T>Эквивалентны; выберите стиль команды
as any внутри generic-утилитыСохраняет дыры в типах — сузить через extends

Практика

  1. Реализуйте MyPick<T, K> и MyOmit<T, K> и сравните с встроенными.
  2. Напишите function pluck<T, K extends keyof T>(items: T[], key: K): T[K][].
  3. Сделайте InMemoryRepository<T extends Entity> с методом update(id, patch: Partial<T>).
  4. Реализуйте три WebhookHandler для payment.succeeded, payment.failed, refund.created и поймайте ошибку на chargeId в refund — §сопоставленные типы.
  5. Добавьте в реестр единиц cm и проверьте, что convert не принимает to из другой размерности.
  6. Для AppState выведите PathType<AppState, "user.preferences.lang"> и убедитесь, что неверный путь даёт ошибку компиляции.
  7. Замените две похожие функции разного типа одной generic.

Смежные статьи