Задача: описать общий тип
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?
- Можно ли обобщить не только по типу, но и по названию ключей?
- Как добавить типизацию для случаев, где
createdAt—Date, аgetId—string | number?
- В каких случаях union проще и понятнее, чем generic?
🚩 Красные флаги
- «Просто сделать union» без объяснения проблем масштабируемости.
- Не умеет объяснить, зачем нужен
extends.
- Не знает, что generic нужно указывать явно.
- Путает
extendsс наследованием классов.
🛠 Практика
- Напиши тип
Pair<T>, который принимает массивы одинакового типа:[1, 2]✅[1, "2"]❌
📚 Источники / ссылки
typescriptlangDocumentation - GenericsDocumentation - Generics
Types which take parameters