Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
10 подробных ответов
01Зачем нужен TypeScript поверх JavaScript и что такое структурная (утиная) типизация?
junior
Короткий ответ: TypeScript добавляет статическую проверку типов поверх JS: ошибки ловятся в редакторе и на этапе сборки, а не в рантайме. Типизация в нём структурная — совместимость определяется формой объекта (набором полей), а не именем типа или явным implements.
Подробно:
- Что даёт TS — автодополнение и навигация, безопасный рефакторинг, самодокументируемые контракты, отлов опечаток и
undefinedдо запуска. - Структурная vs номинальная — в Java/C# типы совместимы по имени (номинальная типизация). В TS — если объект имеет нужные поля, он подходит, даже если создан без знания о целевом типе.
- Стирание типов — типы существуют только при компиляции, в рантайме это обычный JS. Нельзя проверить тип через
instanceofдля интерфейса.
interface Point { x: number; y: number }
function len(p: Point): number {
return Math.hypot(p.x, p.y);
}
// объект не объявлял implements Point, но подходит по форме
const v = { x: 3, y: 4, label: "v" };
len(v); // OK — структурная типизация: лишнее поле label не мешает
⚠️ Частая ошибка: считать, что TS защищает в рантайме. Данные из API нужно валидировать (zod и т.п.) — компилятор верит вашим аннотациям на слово.
02В чём разница между type и interface и когда что выбирать?
middle
Короткий ответ: Оба описывают форму объекта и почти взаимозаменяемы. interface поддерживает declaration merging и идиоматичен для публичного объектного API; type мощнее — умеет юнионы, пересечения, кортежи, mapped- и conditional-типы. Для объектов выбор стилистический, для всего остального нужен type.
Подробно:
| Возможность | interface |
type |
|---|---|---|
| Форма объекта | да | да |
| Юнион / кортеж / примитив | нет | да |
| Declaration merging | да (две одноимённые сливаются) | нет (ошибка дубликата) |
| extends / пересечение | extends |
& |
| Mapped / conditional | нет | да |
// declaration merging: расширяем чужой интерфейс
interface Window { analytics?: Analytics }
// type — только так выражаются юнионы и утилиты
type Status = "idle" | "loading" | "done";
type Nullable<T> = T | null;
⚠️ Частая ошибка: думать, что merging — всегда плюс. В прикладном коде неожиданное слияние одноимённых интерфейсов прячет баги; для замкнутых типов берите type.
03Что такое юнионы, пересечения, нарроуинг и discriminated unions?
middle
Короткий ответ: Юнион A | B — значение одного из типов; пересечение A & B — объект со свойствами обоих. Нарроуинг — сужение юниона до конкретного варианта по проверке в коде. Discriminated union — юнион объектов с общим литеральным полем-дискриминантом, по которому компилятор безопасно сужает тип.
Подробно:
- Сужение по проверкам —
typeof x === "string","field" in obj,x instanceof Cls, проверка наnull. После проверки в ветке тип уже сужен. - Дискриминант — общее поле-литерал (
kind,type,status).switchпо нему даёт исчерпывающую проверку. - Exhaustiveness —
default-ветка сneverловит непокрытые варианты на этапе компиляции.
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2; // тип сужен до circle
case "rect": return s.w * s.h;
default:
const _exhaustive: never = s; // ошибка, если забыли вариант
return _exhaustive;
}
}
⚠️ Частая ошибка: опираться на пользовательский type guard x is T без реальной проверки внутри — он подавляет ошибку, но не гарантирует корректность в рантайме.
04Что такое дженерики, как работают constraints (extends) и почему они лучше any?
middle
Короткий ответ: Дженерики — параметры-типы, которые делают функцию или тип переиспользуемым без потери типобезопасности: связь между входом и выходом сохраняется. T extends ... ограничивает параметр, давая доступ к нужным свойствам. В отличие от any, дженерик не выключает проверку типов, а переносит её на место вызова.
Подробно:
- Сохранение связей —
identity<T>(x: T): Tвозвращает ровно тот тип, что приняла;anyпотерял бы его. - Constraints —
T extends { length: number }гарантирует наличие поля внутри тела функции. - Вывод по ключам —
K extends keyof Tсвязывает ключ и тип возвращаемого значения. - Значения по умолчанию —
<T = string>задаёт дефолтный аргумент типа.
// без констрейнта компилятор не знает, что у T есть .length
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // тип результата = T[K], а не any
}
prop({ id: 1, name: "a" }, "name"); // string
⚠️ Частая ошибка: добавлять <T>, который встречается в сигнатуре лишь один раз — он бесполезен и эквивалентен any. Параметр-тип оправдан, только когда связывает два или более места.
05Какие встроенные utility-типы вы знаете и для чего нужны Partial, Pick, Omit, Record, ReturnType?
middle
Короткий ответ: Utility-типы — готовые трансформации типов из стандартной библиотеки. Они выводят новые типы из существующих, не дублируя определения: Partial делает поля необязательными, Pick/Omit отбирают поля, Record строит словарь, ReturnType извлекает тип возврата функции.
Подробно:
| Утилита | Что делает |
|---|---|
Partial<T> |
все поля необязательны |
Required<T> |
все поля обязательны |
Readonly<T> |
поля только для чтения |
Pick<T, K> |
оставить только ключи K |
Omit<T, K> |
убрать ключи K |
Record<K, V> |
объект с ключами K и значениями V |
ReturnType<F> |
тип значения, возвращаемого F |
interface User { id: number; name: string; email: string }
type UserPatch = Partial<User>; // { id?, name?, email? }
type UserCard = Pick<User, "id" | "name">; // { id, name }
type PublicUser = Omit<User, "email">; // { id, name }
type UsersById = Record<number, User>; // { [k: number]: User }
function load() { return { ok: true }; }
type LoadResult = ReturnType<typeof load>; // { ok: boolean }
⚠️ Частая ошибка: копировать поля руками вместо Pick/Omit. Производные типы автоматически следуют за изменениями исходного — ручная копия рассинхронизируется.
06В чём разница между any, unknown и never и почему unknown предпочтительнее any?
senior
Короткий ответ: any отключает проверку типов — значение можно использовать как угодно, типобезопасность теряется. unknown — типобезопасный верхний тип: принимает что угодно, но ничего нельзя сделать, пока не сузишь тип. never — нижний тип, значений не существует (недостижимый код, исчерпывающие проверки).
Подробно:
| Тип | Можно присвоить ему | Можно использовать без сужения | Назначение |
|---|---|---|---|
any |
всё | да (проверки нет) | побег из системы типов |
unknown |
всё | нет — нужно сузить | безопасный ввод извне |
never |
ничего | — | недостижимость, exhaustiveness |
function parse(json: string): unknown {
return JSON.parse(json);
}
const data = parse('{"id":1}');
// data.id; // ошибка: object is of type 'unknown'
if (typeof data === "object" && data && "id" in data) {
// здесь тип сужен — обращение безопасно
}
function fail(msg: string): never { throw new Error(msg); }
⚠️ Частая ошибка: типизировать значения из JSON.parse/fetch как any ради удобства — это разносит небезопасные данные по всему коду. Возвращайте unknown и валидируйте на границе.
07Enum, юнион строковых литералов или const-объект с as const — что выбрать и почему?
middle
Короткий ответ: Для набора фиксированных значений чаще всего лучше юнион строковых литералов или const-объект с as const: они не генерируют рантайм-код и легко сериализуются. Классический enum создаёт реальный объект в JS и имеет особенности (например, числовые enum допускают любое число), поэтому его берут реже.
Подробно:
| Подход | Рантайм-код | Перебор значений | Особенности |
|---|---|---|---|
enum |
да (объект) | да | numeric-enum небезопасен; const-enum инлайнится |
| Юнион литералов | нет | нет (только тип) | минимум кода, идеален как тип параметра |
as const-объект |
да (обычный объект) | да | значения + тип из одного источника |
// const-объект как источник истины для значений и типа
const Role = {
Admin: "admin",
User: "user",
} as const;
type Role = (typeof Role)[keyof typeof Role]; // "admin" | "user"
function setRole(r: Role) {/* ... */}
setRole(Role.Admin); // и значение, и тип согласованы
⚠️ Частая ошибка: забыть as const — тогда поля выводятся как string, и тип Role схлопывается в string, теряя литеральную точность.
08Как типизировать в React: пропсы, children, обработчики событий, дженерик-компоненты и хуки?
senior
Короткий ответ: Пропсы описывают через type/interface; children — это React.ReactNode; обработчики берут из типов событий (React.ChangeEvent<HTMLInputElement>). Хуки выводят тип сами, но useState/useRef часто нужно типизировать явно дженериком, особенно при стартовом null.
Подробно:
- Пропсы — отдельный
type Props; деструктуризация с дефолтами вместоdefaultProps. - children —
React.ReactNode(что угодно рендерящееся), а неJSX.Element. - События — конкретный тип:
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void. - Дженерик-компонент — параметр-тип на функции для переиспользуемых списков.
- Хуки —
useState<T>(),useRef<T>(null); reducer типизируется через тип состояния и action.
type Props<T> = {
items: T[];
render: (item: T) => React.ReactNode;
children?: React.ReactNode;
};
function List<T>({ items, render }: Props<T>) {
const inputRef = React.useRef<HTMLInputElement>(null);
const [active, setActive] = React.useState<T | null>(null);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) =>
setActive(null);
return <ul>{items.map((it, i) => <li key={i}>{render(it)}</li>)}</ul>;
}
⚠️ Частая ошибка: useRef<HTMLInputElement>() без null даёт неизменяемый ref, несовместимый с ref-атрибутом. Для DOM-рефов всегда инициализируйте useRef<T>(null).
09Что такое keyof, typeof, indexed access и mapped-типы для вывода типов из других типов?
middle
Короткий ответ: Это операторы уровня типов, которые позволяют не дублировать определения. keyof T — юнион ключей типа; typeof value — извлекает тип из значения; T[K] (indexed access) — тип конкретного поля; mapped-тип { [K in keyof T]: ... } перебирает ключи и строит новый тип.
Подробно:
keyof—keyof User→"id" | "name"; основа типобезопасного доступа по ключу.typeof— переход из мира значений в мир типов:typeof config.- Indexed access —
User["id"]→number; работает и с юнионом ключей. - Mapped-тип — пройтись по ключам и трансформировать: добавить
readonly,?, поменять тип значения.
interface User { id: number; name: string }
type Keys = keyof User; // "id" | "name"
type IdType = User["id"]; // number
// mapped-тип: все поля становятся опциональными (свой Partial)
type MyPartial<T> = { [K in keyof T]?: T[K] };
// вывести тип из значения через typeof
const defaults = { theme: "dark", retries: 3 };
type Config = typeof defaults; // { theme: string; retries: number }
⚠️ Частая ошибка: писать тип объекта руками, когда уже есть значение-источник. typeof + mapped-типы выводят типы автоматически и не дают им разойтись с реальностью.
10Что такое файлы деклараций (.d.ts), ambient-объявления и как типизировать нетипизированную библиотеку?
senior
Короткий ответ: .d.ts — файлы только с типами, без реализации; они описывают форму существующего JS-кода. Ambient-объявления (declare) сообщают компилятору о сущностях, существующих в рантайме (глобалы, модули) без их определения. Нетипизированную библиотеку покрывают через @types/*, локальный .d.ts или declare module.
Подробно:
@types/*из DefinitelyTyped — первый выбор:npm i -D @types/lodash.- Локальный
.d.ts— если типов нет, опишите модуль черезdeclare module "lib". - Ambient-глобалы —
declare global { interface Window { ... } }для расширения окружения. - Быстрая заглушка —
declare module "lib";даёт модулю типany(временно, без безопасности).
// types/untyped-lib.d.ts — описываем чужой модуль
declare module "untyped-lib" {
export function greet(name: string): string;
export const version: string;
}
// расширяем глобальный объект
declare global {
interface Window {
__APP_VERSION__: string;
}
}
export {}; // делает файл модулем, чтобы global merging сработал
⚠️ Частая ошибка: оставлять пустой declare module "lib"; навсегда — он даёт any всему API и убивает типобезопасность. Это заглушка, а не решение; опишите реальные сигнатуры.
Источники
Источники и редакционная политика
Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.
От чтения к воспроизведению
Отрепетируйте полный цикл интервью.
RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.