Перейти к содержанию
Фронтенд

10 вопросов по теме «Frontend: TypeScript» на собеседовании

В этом материале — 10 вопросов из русской колоды RecallDeck по теме «Frontend: TypeScript». Сначала сформулируйте короткий ответ сами, затем откройте подробный разбор и проверьте примеры, ограничения и отказные случаи.

10 мин чтения10 подробных ответовПроверено 24 августа 2026
Главная мысль

Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.

Вопросы и ответы

10 подробных ответов

01

Зачем нужен TypeScript поверх JavaScript и что такое структурная (утиная) типизация?

Короткий ответ: TypeScript добавляет статическую проверку типов поверх JS: ошибки ловятся в редакторе и на этапе сборки, а не в рантайме. Типизация в нём структурная — совместимость определяется формой объекта (набором полей), а не именем типа или явным implements.

Подробно:

  1. Что даёт TS — автодополнение и навигация, безопасный рефакторинг, самодокументируемые контракты, отлов опечаток и undefined до запуска.
  2. Структурная vs номинальная — в Java/C# типы совместимы по имени (номинальная типизация). В TS — если объект имеет нужные поля, он подходит, даже если создан без знания о целевом типе.
  3. Стирание типов — типы существуют только при компиляции, в рантайме это обычный 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 и когда что выбирать?

Короткий ответ: Оба описывают форму объекта и почти взаимозаменяемы. 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?

Короткий ответ: Юнион A | B — значение одного из типов; пересечение A & B — объект со свойствами обоих. Нарроуинг — сужение юниона до конкретного варианта по проверке в коде. Discriminated union — юнион объектов с общим литеральным полем-дискриминантом, по которому компилятор безопасно сужает тип.

Подробно:

  1. Сужение по проверкамtypeof x === "string", "field" in obj, x instanceof Cls, проверка на null. После проверки в ветке тип уже сужен.
  2. Дискриминант — общее поле-литерал (kind, type, status). switch по нему даёт исчерпывающую проверку.
  3. Exhaustivenessdefault-ветка с 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?

Короткий ответ: Дженерики — параметры-типы, которые делают функцию или тип переиспользуемым без потери типобезопасности: связь между входом и выходом сохраняется. T extends ... ограничивает параметр, давая доступ к нужным свойствам. В отличие от any, дженерик не выключает проверку типов, а переносит её на место вызова.

Подробно:

  1. Сохранение связейidentity<T>(x: T): T возвращает ровно тот тип, что приняла; any потерял бы его.
  2. ConstraintsT extends { length: number } гарантирует наличие поля внутри тела функции.
  3. Вывод по ключамK extends keyof T связывает ключ и тип возвращаемого значения.
  4. Значения по умолчанию<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?

Короткий ответ: 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?

Короткий ответ: 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 и валидируйте на границе.

07

Enum, юнион строковых литералов или const-объект с as const — что выбрать и почему?

Короткий ответ: Для набора фиксированных значений чаще всего лучше юнион строковых литералов или 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, обработчики событий, дженерик-компоненты и хуки?

Короткий ответ: Пропсы описывают через type/interface; children — это React.ReactNode; обработчики берут из типов событий (React.ChangeEvent<HTMLInputElement>). Хуки выводят тип сами, но useState/useRef часто нужно типизировать явно дженериком, особенно при стартовом null.

Подробно:

  1. Пропсы — отдельный type Props; деструктуризация с дефолтами вместо defaultProps.
  2. childrenReact.ReactNode (что угодно рендерящееся), а не JSX.Element.
  3. События — конкретный тип: onChange: (e: React.ChangeEvent<HTMLInputElement>) => void.
  4. Дженерик-компонент — параметр-тип на функции для переиспользуемых списков.
  5. Хуки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-типы для вывода типов из других типов?

Короткий ответ: Это операторы уровня типов, которые позволяют не дублировать определения. keyof T — юнион ключей типа; typeof value — извлекает тип из значения; T[K] (indexed access) — тип конкретного поля; mapped-тип { [K in keyof T]: ... } перебирает ключи и строит новый тип.

Подробно:

  1. keyofkeyof User"id" | "name"; основа типобезопасного доступа по ключу.
  2. typeof — переход из мира значений в мир типов: typeof config.
  3. Indexed accessUser["id"]number; работает и с юнионом ключей.
  4. 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-объявления и как типизировать нетипизированную библиотеку?

Короткий ответ: .d.ts — файлы только с типами, без реализации; они описывают форму существующего JS-кода. Ambient-объявления (declare) сообщают компилятору о сущностях, существующих в рантайме (глобалы, модули) без их определения. Нетипизированную библиотеку покрывают через @types/*, локальный .d.ts или declare module.

Подробно:

  1. @types/* из DefinitelyTyped — первый выбор: npm i -D @types/lodash.
  2. Локальный .d.ts — если типов нет, опишите модуль через declare module "lib".
  3. Ambient-глобалыdeclare global { interface Window { ... } } для расширения окружения.
  4. Быстрая заглушка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, архитектуру и поведенческие истории.

Начать подготовку

Продолжить подготовку

Библиотека собеседований RecallDeck

Подробные русские ответы, разборы этапов найма и планы подготовки для российского IT-рынка.

RSS