Интервью-вопрос
Что такое дженерики
Дженерики позволяют передавать тип как параметр и сохранять строгую типизацию в переиспользуемом коде. На практике важно не путать их с `any` и помнить, что они не проверяют данные в runtime.
- Добавлен
- Редакция
Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.
Мини-квиз
Проверка перед разбором
Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.
Вопрос 1 из 50 правильно
Разбор
Разобраться, а не зазубрить
Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.
Базовая идея
Дженерик можно воспринимать как переменную для типа. Обычная переменная хранит значение, а generic-параметр хранит тип, который TypeScript подставит при использовании.
Самый простой пример показывает не саму функцию, а связь между входом и выходом:
function identity<T>(value: T): T {
return value;
}
const name = identity("Ada"); // string
const count = identity(3); // numberЗдесь функция одна, но TypeScript не теряет конкретный тип. Если бы вместо T был any, редактор хуже подсказывал бы свойства, а ошибки могли бы пройти до runtime.
Как лучше сказать на интервью
Можно ответить так:
Дженерики это способ параметризовать код типом. Я могу написать универсальную функцию или интерфейс, а конкретный тип передать или вывести при использовании. Это помогает не дублировать код и не уходить в
any, потому что TypeScript сохраняет связь между аргументами, результатом и полями объекта.
После этого добавьте пример из своей практики. Для frontend-разработчика удобно говорить про API-ответ, хук, форму, таблицу или список. Так ваш ответ звучит не как определение из учебника, а как рабочее понимание инструмента.
Где это полезно во frontend-коде
Частый пример - общая оболочка ответа API. Структура ответа одинаковая, но поле data бывает разным.
type ApiResponse<T> = {
data: T;
status: "success" | "error";
message?: string;
};
type User = {
id: string;
name: string;
};
async function fetchJson<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
if (!response.ok) {
throw new Error("Request failed");
}
return response.json() as Promise<ApiResponse<T>>;
}
const result = await fetchJson<User>("/api/me");
result.data.name;Польза в том, что ApiResponse не нужно копировать для каждого ресурса. Но есть важная граница: fetchJson<User> не проверяет, что сервер реально вернул User. Приведение типа после response.json() только сообщает TypeScript, чему вы доверяете. Оно не защищает UI от пустого ответа, HTML вместо JSON или поля другого формата. В реальном компоненте также обработайте loading, error и empty state. Если ответ приходит из внешнего API или влияет на деньги, права доступа или важный UI, добавьте runtime-валидацию.
Ограничения через extends
Generic без ограничений может быть чем угодно. Поэтому TypeScript не даст обратиться к свойству, которое не гарантировано.
Плохой пример:
function getId<T>(item: T) {
return item.id;
}Такой код не пройдет строгую проверку TypeScript. Если обойти ошибку через as any, в runtime можно получить undefined, неверный ключ в списке React или запрос на URL с пустым id. Правильнее описать минимальный контракт:
function getId<T extends { id: string }>(item: T): string {
return item.id;
}
getId({ id: "u1", name: "Ada" });Теперь функция все еще универсальная. Она принимает любой объект с дополнительными полями, но требует id. Это хорошая формулировка для интервью: generic не означает "любой без правил", он может быть ограничен контрактом.
Как выбрать между generic, union и overload
Как выбрать подход
Используйте дженерик, например `function identity<T>(value: T): T`.Часто достаточно union-типа, например `"idle" | "loading" | "error"`.Рассмотрите перегрузки, чтобы описать разные входы и результаты.Не заменяйте проблему дженериком. Найдите точный тип или добавьте runtime-проверку.На интервью не стоит говорить, что дженерики нужны везде. Они нужны там, где тип должен подставляться и сохраняться. Если у вас фиксированный список значений, union обычно проще. Если разные сигнатуры ведут себя по-разному, иногда честнее описать перегрузки.
Пример, где union лучше generic:
type LoadStatus = "idle" | "loading" | "success" | "error";Здесь нет смысла делать абстрактный параметр типа. Вам нужна не универсальность, а строгий список допустимых состояний. Это снижает риск опечаток, лишних состояний и неправильной обработки UI.
Типичная ловушка с API
Самая частая ошибка в ответе - сказать, что generic делает данные безопасными. TypeScript проверяет ваш код до запуска, но не проверяет реальный ответ сервера.
const user = await fetchJson<User>("/api/me");
// TypeScript верит, что name есть.
user.data.name.toUpperCase();Если сервер вернул { "username": "ada" }, типизация не спасет от ошибки логики. UI может упасть на рендере профиля или показать пустое имя без понятной ошибки. Безопаснее проверять форму ответа до записи в состояние и отдельно показывать loading, error и empty state. Если компонент делает запрос в useEffect, generic тоже не отменяет запрос и не защищает от race condition. Для этого нужны AbortController, cleanup или проверка актуальности ответа. Безопасный вывод такой: generic помогает описать ожидаемый контракт, но для внешних данных нужна проверка ответа, особенно если API нестабилен или контролируется другой командой.
Практический вывод
В сильном ответе держите три шага. Сначала дайте определение: параметр типа для универсального кода. Потом покажите пользу: сохраняется связь между типами без any. Затем назовите границу: для свойств нужен extends, а для данных из runtime нужна отдельная проверка.
Если вы пишете код на React и TypeScript, дженерики часто встречаются в хуках, компонентах списков, формах, таблицах и типах API. Но хороший frontend-код не обязан быть максимально абстрактным. Он должен быть понятным для команды и достаточно строгим, чтобы ловить ошибки до запуска приложения.
Частые ошибки
Где обычно ошибаются
Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.
- 1
Заменять `any` бессмысленным `T`
Если тип-параметр нигде не связывает значения, он не добавляет безопасности. Например, функция, которая принимаетTи всегда возвращает строку, часто не нуждается в дженерике. Лучше оставить конкретные типы и не создавать ложное ощущение универсальности. - 2
Не ставить ограничения там, где они нужны
Если вы обращаетесь кvalue.id, TypeScript должен знать, чтоidсуществует. Для этого нуженT extends { id: string }или другой явный контракт. Иначе код либо не скомпилируется, либо вы начнете обходить типизацию черезas any. - 3
Путать типизацию с валидацией данных
fetchJson<User>()говорит TypeScript, каким типом вы считаете результат, но не проверяет ответ сервера. Если API вернет поле не того формата, баг проявится в UI уже в runtime. Для ненадежных данных добавляйте проверку схемой или ручной guard. - 4
Делать слишком сложные generic-типы
Вложенные условные типы и длинные цепочки параметров могут быть точными, но плохо читаются. На интервью это звучит слабее, если вы не можете объяснить пользу. В рабочем коде лучше разделить типы на именованные части и оставить простой публичный API. - 5
Использовать generic там, где нужен union
Если вариантов мало и они известны заранее, union часто проще и понятнее. Например, статус загрузки лучше описать как"idle" | "loading" | "success" | "error", а не как абстрактныйTStatus. Так код легче читать и безопаснее расширять.
Follow-up
Что могут спросить дальше
Короткие ответы на вопросы, которыми проверяют понимание дженериков, ограничений и границ TypeScript.
Живые ответы
Видео с похожим вопросом
Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.
Пока видео нет. Когда появятся подходящие публичные интервью, добавим их в этот блок, чтобы можно было сравнить разбор с тем, как отвечают реальные кандидаты.
Как распределяются по очереди задач синхронный код
Разбор вопроса «Как распределяются по очереди задач синхронный код» для Frontend Developer: что проверяет интервьюер, ключевые тезисы, практические примеры и частые ошибки.
Что такое Omit в TypeScript 😎
Omit создает новый тип без указанных ключей исходного типа. Разбираем, где он полезен во frontend-коде, чем отличается от Pick и почему он не удаляет поля из объекта во время выполнения.