Дженерики в TypeScript
Дальше: Паттерны · Объекты и классы · Справочник — утилитарные типы
Теория: Обобщения и обобщённое программирование — определения, зачем нужны generics, отличия языков.
Дженерики (generics) — параметры типа: одна функция или класс работает с разными данными, компилятор сохраняет связь между аргументом и результатом. Без них пришлось бы дублировать код или опираться на any. Удобный порядок — функции, эта статья, async Promise<T>.
Аналогия в C# — обобщения. Указатель в JS-курсе — JS-курс: дженерики.
Связанный выбор — один аргумент сужает другой
В выразительном TypeScript часто работает одна схема — первый выбор (ключ, имя события, исходная единица, статус ответа) задаёт второй (тип значения, payload, целевая единица, доступные поля).
| Первый выбор | Второй сужается до | Где разобрано |
|---|---|---|
ключ K | тип T[K] | ниже §keyof, updater |
имя события E | PayloadMap[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 | Произвольный тип |
K | Key (ключ) |
V | Value (значение) |
E | Element |
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}.${…}`): обход вложенных объектов на уровне типов.PathType—infer 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 |
Практика
- Реализуйте
MyPick<T, K>иMyOmit<T, K>и сравните с встроенными. - Напишите
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][]. - Сделайте
InMemoryRepository<T extends Entity>с методомupdate(id, patch: Partial<T>). - Реализуйте три
WebhookHandlerдляpayment.succeeded,payment.failed,refund.createdи поймайте ошибку наchargeIdв refund — §сопоставленные типы. - Добавьте в реестр единиц
cmи проверьте, чтоconvertне принимаетtoиз другой размерности. - Для
AppStateвыведитеPathType<AppState, "user.preferences.lang">и убедитесь, что неверный путь даёт ошибку компиляции. - Замените две похожие функции разного типа одной generic.
Смежные статьи
- Функции —
filter<T>, типы callback - Типы — union, utility types
- Коллекции —
Map,Set,Record - Паттерны — discriminated union + generics
- Справочник · Справочник — утилитарные и расширенные типы