Практика: обобщение типа

Задача: описать общий тип Obj, чтобы объект манипулировал либо строками, либо числами. Комбинации типов внутри одного экземпляра не допускаются.
type Obj = { getId: () => number, createdAt: number, }; // ✅ const a: Obj = { getId: () => 1, createdAt: 1761566635754 }; // ✅ const b: Obj = { getId: () => "cool-id", createdAt: "2025-10-27T12:04:36.762Z", }; // ❌ const c: Obj = { getId: () => 1, createdAt: "2025-10-27T12:04:36.762Z", };
 
 
🎯 Зачем спрашивают
  • Проверить, понимает ли кандидат принцип обобщения типов (generics) в TypeScript — одну из ключевых концепций языка.
  • Посмотреть, может ли он отличить union-типы от параметрических (generic) и объяснить, когда какой подход уместен.
  • Оценить глубину типового мышления: умеет ли кандидат выстраивать связи между полями и избегать «разъезда» типов.
  • Проверить умение выбирать решение в зависимости от контекста — простая задача → union, масштабируемая архитектура → generic.
 
📝 Ответ
Коротко:
Есть два решения:
  • в лоб (явное перечисление)
  • “по-умному” (параметризация)
 
Решение в лоб — перечислить все варианты, сделать union тип:
type Obj = | { getId: () => number, createdAt: number, } | { getId: () => string, createdAt: string, };
Решение работает, но имеет свои минусы. Если полей много, будет много копипасты.
 
А так же возможна потеря строгости типа:
const test: Obj = Math.random() > 0.5 ? { getId: () => 1, createdAt: 123 } : { getId: () => "x", createdAt: "y" }; test.getId(); // возвращает string | number → теряем строгость
 
✅ Плюсы
❌ Минусы
Подходит для простых типов
Копипаста
Потеря строгости типа

 
Решение “по-умному” — реализовать через generic:
type Obj<T extends string | number> = { getId: () => T, createdAt: T, };
Мы объявляем переменную T и через extends задаем возможные типы.
type Obj<T extends string | number> = { getId: () => T, createdAt: T, }; // ✅ const a: Obj<number> = { getId: () => 1, createdAt: 1761566635754 }; // ✅ const b: Obj<string> = { getId: () => "cool-id", createdAt: "2025-10-27T12:04:36.762Z", }; // ❌ const c: Obj<string> = { getId: () => 1, createdAt: "2025-10-27T12:04:36.762Z", };
 
✅ Плюсы
❌ Минусы
Нет копипасты
Требует понимания вывода типов
Расширяемость: можно добавить больше ограничений (T extends string | number | bigint)
Ошибка может не отлавливаться, если generic не задан явно
Композиция: можно переиспользовать тип в других местах
Новичкам сложно читать такие типы (снижение DX)
 
⚖️ Компромиссы
Union проще для чтения, но плохо масштабируется и теряет строгую связь между полями.
Generic требует большего понимания, но даёт мощную систему ограничений.
 
🔎 Встречные вопросы
  • Что будет, если заменить extends string | number на extends string & number?
  • Можно ли обобщить не только по типу, но и по названию ключей?
  • Как добавить типизацию для случаев, где createdAtDate, а getIdstring | number?
  • В каких случаях union проще и понятнее, чем generic?
 
🚩 Красные флаги
  • «Просто сделать union» без объяснения проблем масштабируемости.
  • Не умеет объяснить, зачем нужен extends.
  • Не знает, что generic нужно указывать явно.
  • Путает extends с наследованием классов.
 
🛠 Практика
  • Напиши тип Pair<T>, который принимает массивы одинакового типа: [1, 2][1, "2"]
 
 
📚 Источники / ссылки