Зачем нужны дженерики
Дженерик позволяет написать функцию или класс, работающие с разными
типами, не теряя информации о типе. Сравним с any:
// Без дженерика — теряем тип
function identityAny(value: any): any {
return value;
}
const x = identityAny(42); // тип — any
const y = identityAny("hi"); // тип — any
x.toFixed(); // ОК для TS, но упадёт, если value было числом
y.toUpperCase(); // то же самое
С дженериком тип сохраняется:
function identity<T>(value: T): T {
return value;
}
const a = identity(42); // тип — number
const b = identity("hi"); // тип — string
a.toFixed(); // ОК
b.toUpperCase(); // ОК
// a.toUpperCase(); // Ошибка: числа не имеют toUpperCase
Буква T — это просто имя параметра типа. По соглашению
используют T (Type), K (Key), V
(Value), E (Element), но это не обязательно.
Несколько параметров типа
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair("Анна", 28); // [string, number]
Явное указание типа
Обычно TypeScript выводит тип по аргументу, но его можно задать явно:
const value = identity<number>(42);
const items = pair<string, boolean>("flag", true);
Дженерики в интерфейсах
Классический пример — обёртка над значением, тип которого известен заранее («коробка»):
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "hi" };
// numberBox.value.toFixed(); // ОК
// numberBox.value.toUpperCase(); // Ошибка
Кейс: типобезопасный API-ответ
interface ApiResponse<T> {
status: "ok" | "error";
data: T;
meta?: { page: number; total: number };
}
interface User {
id: number;
name: string;
}
const response: ApiResponse<User> = {
status: "ok",
data: { id: 1, name: "Анна" },
};
// TS знает, что response.data — это User
console.log(response.data.name);
Кейс: коллекция с указанным типом элементов
interface PaginatedList<T> {
items: T[];
total: number;
page: number;
}
const usersPage: PaginatedList<User> = {
items: [{ id: 1, name: "Анна" }],
total: 1,
page: 1,
};
Ограничения (constraints)
Ключевое слово extends ограничивает множество допустимых
типов. Например, требуем, чтобы у значения было свойство length:
function getLength<T extends { length: number }>(value: T): number {
return value.length;
}
getLength("hello"); // 5 — у строки есть length
getLength([1, 2, 3]); // 3 — у массива есть length
// getLength(42); // Ошибка: у числа нет length
keyof — тип «ключей объекта»
Часто нужно требовать, чтобы ключ существовал в типе. keyof T
даёт объединение всех ключей типа T:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Анна", age: 28 };
getProperty(user, "name"); // string — TS знает тип значения
getProperty(user, "age"); // number
// getProperty(user, "email"); // Ошибка: такого ключа нет
T[K] — это индексированный доступ к типу: «тип
значения по ключу K в объекте T».
Кейс: типобезопасный setter
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
obj[key] = value;
}
const config = { port: 3000, host: "localhost" };
setProperty(config, "port", 8080); // OK
// setProperty(config, "port", "8080"); // Ошибка: port — number
Дженерики в классах
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numbers = new Stack<number>();
numbers.push(1);
numbers.push(2);
const n = numbers.pop(); // number | undefined
const strings = new Stack<string>();
strings.push("hi");
// strings.push(42); // Ошибка: ожидается string
Кейс: кэш с ключами любого типа
class Cache<K, V> {
private store = new Map<K, V>();
set(key: K, value: V): void {
this.store.set(key, value);
}
get(key: K): V | undefined {
return this.store.get(key);
}
has(key: K): boolean {
return this.store.has(key);
}
}
// Ключи — числа, значения — строки
const userCache = new Cache<number, string>();
userCache.set(1, "Анна");
userCache.set(2, "Борис");
// Ключи — строки, значения — объекты
const apiCache = new Cache<string, { status: number }>();
apiCache.set("/users", { status: 200 });
Параметры типа по умолчанию
У параметра типа может быть значение по умолчанию:
interface ApiResponse<T, E = Error> {
data?: T;
error?: E;
}
// E по умолчанию = Error
const r1: ApiResponse<User> = { data: { id: 1, name: "x" } };
// E можно переопределить
const r2: ApiResponse<User, string> = { error: "не найдено" };
typeof — тип по значению
Иногда нужно получить тип из конкретного значения:
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
};
type Config = typeof config;
// { apiUrl: string; timeout: number; retries: number }
function init(c: Config): void {
// ...
}
init(config);
Кейс: константы и literal-типы через as const
const STATUSES = ["new", "paid", "done"] as const;
type Status = typeof STATUSES[number]; // "new" | "paid" | "done"
function setStatus(s: Status): void {
// ...
}
setStatus("new");
// setStatus("closed"); // Ошибка
Кейс: типобезопасный event emitter
Реальный пример — события с типизированными именами и payload. Сначала описываем карту событий, потом дженерик гарантирует соответствие имени и аргументов:
type EventMap = {
login: { userId: number };
logout: undefined;
purchase: { orderId: number; amount: number };
};
class TypedEmitter<Events extends Record<string, unknown>> {
private handlers: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void): void {
(this.handlers[event] ??= []).push(handler);
}
emit<K extends keyof Events>(event: K, payload: Events[K]): void {
this.handlers[event]?.forEach((h) => h(payload));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("login", (p) => {
console.log(p.userId); // TS знает про userId
});
emitter.emit("login", { userId: 1 }); // OK
// emitter.emit("login", { orderId: 1 }); // Ошибка
// emitter.emit("signup", { userId: 1 }); // Ошибка: нет такого события
Типичные ошибки
1. Использование any там, где нужен дженерик
// Плохо: теряем тип
function first(arr: any[]): any {
return arr[0];
}
// Хорошо: тип сохраняется
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
2. Слишком жёсткий constraint
// Избыточно: требует именно { name: string }, хотя достаточно length
function getLen<T extends { name: string }>(v: T): number {
return v.name.length;
}
3. Путаница между параметром типа и обычным параметром
// <T> — параметр типа, не передаётся в рантайме
function f<T>(value: T): T { return value; }
Практика
- Напишите дженерик-функцию
first<T>(arr: T[]): T | undefined. - Реализуйте функцию
groupBy<T>(arr: T[], key: (item: T) => string): Record<string, T[]>. - Создайте класс
Dictionary<V>с методамиset/get/has. - Опишите функцию
pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>(или используйте готовый utility-тип). - Реализуйте простую типобезопасную очередь
Queue<T>сenqueue/dequeue.
Итог
Дженерики — главный инструмент для переиспользуемого типобезопасного кода.
Мы разобрали синтаксис функций, интерфейсов и классов, ограничения через
extends, keyof и индексированный доступ, параметры
типа по умолчанию, typeof и реальные паттерны: API-ответы, кэш и
типизированный event emitter. В следующей главе — модули и конфигурация
проекта.
Комментарии 0
Пока нет комментариев. Станьте первым!