Дженерики: практические примеры

Практические паттерны использования generic-типов в реальных задачах: коллекции, утилиты, API-обёртки, HOF и типобезопасные хранилища — реализованные через параметризованные типы TypeScript.

Зачем нужно

Видеть generics в контексте реальных задач проще, чем изучать синтаксис абстрактно. Эта заметка — коллекция готовых паттернов для копирования и адаптации.

Где используется

  • HTTP-клиенты с типизированными ответами
  • Коллекции и структуры данных
  • HOF: memoize, retry, debounce с сохранением типов
  • Формы и валидация
  • Event emitter с типизированными событиями

Основной контент

Типизированный fetch

async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
  const res = await fetch(url, init);
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
  return res.json() as Promise<T>;
}

interface User { id: number; name: string; email: string }
interface Post { id: number; title: string; body: string }

const user = await fetchJson<User>("/api/users/1");
const posts = await fetchJson<Post>("/api/posts");
// user: User, posts: Post — типобезопасно

Generic коллекция: Stack

class Stack<T> {
  private items: T = ;

  push(item: T): this { this.items.push(item); return this; }
  pop: T | undefined { return this.items.pop(); }
  peek: T | undefined { return this.items.at(-1); }
  isEmpty: boolean { return this.items.length === 0; }
  get size: number { return this.items.length; }
  toArray: T { return [...this.items]; }
}

const stack = new Stack<number>;
stack.push(1).push(2).push(3);
console.log(stack.pop()); // 3

Generic Result тип

type Result<T, E = Error> =
  | { ok: true; data: T }
  | { ok: false; error: E };

function ok<T>(data: T): Result<T> {
  return { ok: true, data };
}

function fail<T, E = Error>(error: E): Result<T, E> {
  return { ok: false, error } as Result<T, E>;
}

async function parseUser(raw: unknown): Promise<Result<User>> {
  if (typeof raw !== "object" || raw === null) {
    return fail(new Error("Invalid data"));
  }
  // ... дополнительная валидация
  return ok(raw as User);
}

Generic memoize

function memoize<Args extends unknown, R>(
  fn: (...args: Args) => R
): (...args: Args) => R {
  const cache = new Map<string, R>;

  return (...args: Args): R => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key)!;
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

function expensiveCalc(n: number): number {
  return n * n; // имитация дорогого вычисления
}

const memoized = memoize(expensiveCalc);
memoized(5); // вычисляет
memoized(5); // из кэша

Типизированный EventEmitter

type EventMap = Record<string, unknown>;

class TypedEmitter<Events extends EventMap> {
  private handlers = new Map<keyof Events, Function>;

  on<K extends keyof Events>(
    event: K,
    handler: (...args: Events[K]) => void
  ): void {
    const list = this.handlers.get(event) ?? ;
    list.push(handler);
    this.handlers.set(event, list);
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
    this.handlers.get(event)?.forEach((fn) => fn(...args));
  }
}

// Использование
const emitter = new TypedEmitter<{
  message: [text: string, from: string];
  error: [err: Error];
}>;

emitter.on("message", (text, from) => {
  // text: string, from: string — выведено!
  console.log(`${from}: ${text}`);
});

emitter.emit("message", "Hello", "Alice");
emitter.emit("error", new Error("oops"));

Generic guard-функция

function assertDefined<T>(
  value: T | null | undefined,
  message = "Value is not defined"
): asserts value is T {
  if (value == null) throw new Error(message);
}

const user: User | null = getUser;
assertDefined(user, "User not found");
// user: User — TypeScript знает
console.log(user.name);

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

  • Дублировать generics без нужды — если функция всегда возвращает string, generic для возвращаемого значения избыточен.
  • Не ограничивать T там где нужен доступ к полямT extends { id: string } даёт доступ к obj.id.
  • Использовать any вместо unknown в HOFany отключает все проверки параметров.
  • Слишком сложные generic — если тип сложно читать, возможно нужен несколько упрощённых функций.

Связанные темы

Ресурсы