Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
10 подробных ответов
Проверка по первоисточникам опубликована для 2 из 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: юнион и утверждение as const стираются при компиляции, но сам const-объект остаётся в JS. Строковые значения удобно сериализовать. Обычный enum тоже создаёт объект; выбор зависит от того, нужны ли значения во время выполнения и какой стиль принят в проекте. В TypeScript 5+ числовой литерал вне enum отклоняется; это не полноценная проверка чисел из внешних данных.
Подробно:
| Подход | Рантайм-код | Перебор значений | Особенности |
|---|---|---|---|
enum |
да (объект) | да | числовой 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Как типизировать nullable DOM-ref в React 19?
senior
Короткий ответ: В React 19 с @types/react 19 используйте useRef<HTMLInputElement>(null) для DOM-ref поля ввода. Получится изменяемый ref, где current имеет тип HTMLInputElement | null. Начальный аргумент обязателен: useRef<HTMLInputElement>() — ошибка типов, а не перегрузка с неизменяемым ref.
Подробно:
import { useRef } from "react";
function SearchField() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} aria-label="Поиск" />
<button onClick={() => inputRef.current?.focus()}>Фокус в поиск</button>
</>
);
}
React записывает DOM-узел в ref во время commit и очищает ссылку при удалении узла. Обработайте null: до монтирования и после удаления узел может отсутствовать. Читайте и изменяйте ref в обработчиках событий или эффектах, когда это уместно; изменение current не вызывает рендер.
Все ref, возвращаемые типами useRef в React 19, изменяемы. В типах React 18 были другие перегрузки, включая readonly-результат при некоторых инициализациях с null; не переносите это различие на React 19. Согласуйте major-версии runtime и пакета типов.
Важно: Если изменение значения должно обновлять интерфейс, используйте state. Изменяемый объект сам по себе не является реактивным состоянием.
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 возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.