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

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

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

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

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

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

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

01

Зачем нужен TypeScript? Что он даёт поверх JavaScript?

Короткий ответ: TypeScript — это надмножество JavaScript со статической типизацией. Он ловит целый класс ошибок на этапе компиляции (до запуска), даёт автодополнение, рефакторинг и самодокументируемость кода.

Подробно:

TypeScript добавляет систему типов поверх JS. Любой валидный JS — валидный TS, но TS даёт:

  • Раннее обнаружение ошибок — опечатки, неверные типы аргументов, обращение к undefined.
  • IntelliSense / автодополнение — IDE знает структуру объектов.
  • Безопасный рефакторинг — переименование, поиск использований по типам.
  • Документация в коде — сигнатуры функций описывают контракт.
// JS: ошибка проявится только в рантайме
function greet(user) {
  return "Hello, " + user.name.toUpperCase();
}
greet({ nme: "Bob" }); // TypeError в рантайме: Cannot read 'toUpperCase' of undefined

// TS: ошибка на этапе компиляции
function greetTs(user: { name: string }) {
  return "Hello, " + user.name.toUpperCase();
}
greetTs({ nme: "Bob" }); // ❌ Error: Object literal may only specify known properties

⚠️ Ловушка: TS-типы существуют только во время компиляции. Они не дают рантайм-гарантий — данные из API, JSON.parse, форм могут не соответствовать заявленному типу. На границах системы нужна рантайм-валидация (например, zod).

02

Что значит «типы стираются при компиляции» (type erasure)?

Короткий ответ: Компилятор tsc проверяет типы, а затем удаляет всю информацию о типах, выдавая обычный JS. В рантайме типов не существует — нельзя проверить тип по интерфейсу через instanceof.

Подробно:

Аннотации типов, интерфейсы, type, дженерики — всё это компилируется в ничто.

interface User {
  id: number;
  name: string;
}

function process<T>(items: T[]): T[] {
  return items;
}

const x: User = { id: 1, name: "Ann" };

Компилируется в:

function process(items) {
  return items;
}
const x = { id: 1, name: "Ann" };
// Никаких следов User, T, аннотаций — всё стёрто

Следствия:

// ❌ Нельзя проверить интерфейс в рантайме — его не существует
if (value instanceof User) {} // Error: 'User' only refers to a type

// ❌ Нельзя получить T в рантайме
function create<T>(): T {
  return new T(); // Error: 'T' only refers to a type
}

// ✅ instanceof работает только с классами (они есть в рантайме)
class Animal {}
if (pet instanceof Animal) {} // OK

⚠️ Ловушка: Из-за стирания типов нельзя «отрефлексировать» тип в рантайме. Поэтому для валидации входящих данных используют схемы (zod, io-ts), которые работают и в рантайме, и выводят TS-тип.

03

Что такое структурная типизация (duck typing)? Чем отличается от номинальной?

Короткий ответ: В TypeScript типы совместимы по структуре (форме), а не по имени. Если объект имеет нужные поля — он подходит, независимо от того, как он объявлен. Это противоположность номинальной типизации (Java, C#), где важно имя/наследование.

Подробно:

interface Point {
  x: number;
  y: number;
}

function printPoint(p: Point) {
  console.log(p.x, p.y);
}

// Не объявлен как Point, но структурно совместим
const coord = { x: 1, y: 2, z: 3 };
printPoint(coord); // ✅ OK — есть x и y, лишний z игнорируется

// Разные интерфейсы с одинаковой структурой взаимозаменяемы
interface Vector { x: number; y: number; }
const v: Vector = { x: 0, y: 0 };
printPoint(v); // ✅ OK

Номинальная типизация потребовала бы явного implements Point.

Эмуляция номинальной типизации в TS (branded types):

type UserId = number & { readonly __brand: "UserId" };
type PostId = number & { readonly __brand: "PostId" };

function getUser(id: UserId) {}

const uid = 1 as UserId;
const pid = 2 as PostId;
getUser(uid); // ✅
getUser(pid); // ❌ нельзя передать PostId вместо UserId

⚠️ Ловушка: Лишние свойства игнорируются при передаче переменной, но при передаче литерала объекта напрямую срабатывает «excess property check»:

printPoint({ x: 1, y: 2, z: 3 }); // ❌ Error: 'z' does not exist in type 'Point'
const c = { x: 1, y: 2, z: 3 };
printPoint(c); // ✅ через переменную — OK
04

Базовые типы: string/number/boolean/null/undefined/void/never/unknown/any

Короткий ответ: Примитивы (string, number, boolean), пустые (null, undefined), void (функция ничего не возвращает), never (значения не существует никогда), unknown (безопасный верхний тип), any (отключение проверок).

Подробно:

let s: string = "hello";
let n: number = 42;          // целые, дробные, бесконечность, NaN
let b: boolean = true;
let nul: null = null;
let und: undefined = undefined;

// void — функция не возвращает значимого результата
function log(msg: string): void {
  console.log(msg);
}

// never — функция никогда не завершается нормально
function fail(msg: string): never {
  throw new Error(msg);
}
function loop(): never {
  while (true) {}
}

// unknown — любое значение, но требует сужения перед использованием
let u: unknown = JSON.parse("...");
// u.toUpperCase(); // ❌ нужно сначала проверить тип

// any — отключает проверки (избегать!)
let a: any = 5;
a.foo.bar.baz; // компилируется, упадёт в рантайме

never также является результатом невозможных типов: string & number = never.

⚠️ Ловушка: void в типе функции-колбэка означает «возвращаемое значение игнорируется», а не «должна вернуть undefined». Поэтому Array.forEach(item => arr.push(item)) валиден, хотя push возвращает number.

05

any vs unknown vs never — глубоко, когда что использовать

Короткий ответ: any — «выключить типизацию» (опасно). unknown — «типобезопасный any»: значение неизвестно, использовать можно только после проверки. never — «значения быть не может»: пустой тип, низший в иерархии.

Подробно:

// any — присваивается куда угодно и принимает что угодно; проверки отключены
let a: any = "text";
a();          // OK для компилятора, упадёт в рантайме
const x: number = a; // OK — заражает остальной код

// unknown — верхний тип: всё присваивается в unknown,
// но unknown не присваивается никуда без сужения
let u: unknown = "text";
// const len = u.length;        // ❌ Object is of type 'unknown'
if (typeof u === "string") {
  const len = u.length;          // ✅ сузили — теперь string
}

// never — низший тип: присваивается во всё, но в него ничто не присвоить
let nv: never;
// nv = 1; // ❌ Type 'number' is not assignable to 'never'

Иерархия присваиваемости: never ⊂ всё остальное ⊂ unknown. any стоит особняком — совместим в обе стороны.

Когда что:

  • unknown — для значений из внешнего мира (API, JSON.parse, catch (e)).
  • never — для проверки полноты (exhaustiveness), невозможных веток, функций, которые всегда бросают.
  • any — почти никогда; временно при миграции с JS.
// never для exhaustiveness
type Shape = { kind: "circle" } | { kind: "square" };
function area(s: Shape) {
  switch (s.kind) {
    case "circle": return 1;
    case "square": return 2;
    default:
      const _exhaustive: never = s; // ❌ если добавят новый kind
      return _exhaustive;
  }
}

⚠️ Ловушка: В TS 4.4+ catch (e) имеет тип unknown (при useUnknownInCatchVariables). Нельзя сразу обращаться к e.message — нужно сузить: if (e instanceof Error) ....

06

any vs unknown — почему unknown безопаснее?

Короткий ответ: any отключает любые проверки и «заражает» весь код — ошибки проскальзывают незаметно. unknown заставляет компилятор требовать проверку типа перед использованием, сохраняя типобезопасность.

Подробно:

function parseAny(json: string): any {
  return JSON.parse(json);
}
const dataAny = parseAny("{}");
dataAny.user.name.toUpperCase(); // ✅ компилятор молчит, рантайм-краш

function parseUnknown(json: string): unknown {
  return JSON.parse(json);
}
const dataUnknown = parseUnknown("{}");
// dataUnknown.user; // ❌ компилятор заставляет проверить
if (
  typeof dataUnknown === "object" &&
  dataUnknown !== null &&
  "user" in dataUnknown
) {
  // безопасный доступ после сужения
}

Ключевая идея: any — «доверься мне» (компилятор отступает). unknown — «докажи мне» (компилятор требует доказательства типа). unknown — это барьер, через который данные проходят только после явной проверки.

⚠️ Ловушка: any распространяется: const x = anyValue.foo делает x тоже any. Один any в цепочке может «съесть» типобезопасность большого участка кода без единой ошибки компиляции.

07

Type vs Interface — различия, когда что использовать

Короткий ответ: Оба описывают форму объекта и почти взаимозаменяемы. interface поддерживает declaration merging и идиоматичен для объектов/классов. type мощнее: union, intersection, кортежи, primitives, mapped/conditional типы.

Подробно:

// interface — расширение через extends
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }

// type — расширение через intersection
type AnimalT = { name: string };
type DogT = AnimalT & { breed: string };

// Только type умеет:
type ID = string | number;                 // union
type Pair = [number, number];              // tuple
type Name = string;                        // alias примитива
type Keys = keyof DogT;                    // операторы типов

// Только interface умеет declaration merging:
interface Window { customProp: string; }
interface Window { anotherProp: number; } // объединяется автоматически

Когда что:

  • interface — публичные API библиотек (расширяемость), объекты, классы (implements), когда нужен declaration merging.
  • type — union/intersection, функции, кортежи, утилитарные/вычисляемые типы.
// interface для классов
interface Repository<T> {
  find(id: string): T | null;
}
class UserRepo implements Repository<User> {
  find(id: string) { return null; }
}

⚠️ Ловушка: Declaration merging у interface может быть неожиданным: два одноимённых интерфейса в одной области видимости молча сольются. У type повторное объявление — ошибка «Duplicate identifier», что иногда предпочтительнее.

08

Чем type отличается от interface на практике?

Короткий ответ: На практике для описания объектов разница минимальна — выбирайте по соглашению команды. Реально важны три момента: declaration merging (только interface), union/tuple/primitive алиасы (только type), и сообщения об ошибках (у interface часто короче и читабельнее).

Подробно:

// Практическое правило многих команд:
// interface — для объектов и контрактов
interface UserProps {
  id: string;
  name: string;
}

// type — когда нужно что-то, что interface не умеет
type Status = "active" | "banned" | "pending";
type Handler = (e: Event) => void;
type Coords = readonly [number, number];

// Расширение глобальных типов — только interface
declare global {
  interface Window {
    __APP_VERSION__: string;
  }
}

Перформанс компилятора: для очень больших пересечений interface extends обычно кэшируется лучше, чем длинные цепочки & в type — но это заметно лишь на огромных проектах.

⚠️ Ловушка: type с & при конфликте полей даёт never для конфликтующего поля (не ошибку сразу), тогда как interface extends с конфликтом типов даёт явную ошибку. Пример: type T = { a: string } & { a: number }a: never.

09

Union (|) и Intersection (&) типы

Короткий ответ: Union (A | B) — значение одного из типов («или»). Intersection (A & B) — значение, удовлетворяющее всем типам сразу («и», объединение полей).

Подробно:

// Union — одно ИЗ
type Result = string | number;
let r: Result = "ok";
r = 42; // тоже OK

// С union доступны только общие члены до сужения
function format(x: string | number) {
  // x.toFixed(); // ❌ нет у string
  return x.toString(); // ✅ есть у обоих
}

// Intersection — объединение требований
type WithId = { id: string };
type WithTimestamps = { createdAt: Date; updatedAt: Date };
type Entity = WithId & WithTimestamps;

const e: Entity = {
  id: "1",
  createdAt: new Date(),
  updatedAt: new Date(),
}; // нужны ВСЕ поля

⚠️ Ловушка: Пересечение примитивов даёт never (string & number невозможно). Также интуиция «union = меньше, intersection = больше» неверна для множеств: union типов = объединение множеств значений (больше значений), intersection объектов = больше требований (меньше подходящих значений).

10

Literal types и const assertions (as const)

Короткий ответ: Литеральные типы — конкретное значение как тип ("red", 42, true). as const делает значение глубоко readonly и сужает типы до литералов.

Подробно:

// Литеральные типы
type Direction = "north" | "south" | "east" | "west";
let d: Direction = "north"; // только из набора

// Без as const: тип расширяется (widening)
const obj1 = { role: "admin" };        // role: string
let m = "GET";                          // string

// С as const: точные литералы + readonly
const obj2 = { role: "admin" } as const; // role: "admin"
const config = {
  method: "POST",
  retries: 3,
} as const;
// тип: { readonly method: "POST"; readonly retries: 3 }

// Полезно для массивов → tuple литералов
const roles = ["admin", "user", "guest"] as const;
type Role = typeof roles[number]; // "admin" | "user" | "guest"

⚠️ Ловушка: Без as const массив ["a", "b"] получает тип string[], и typeof arr[number] будет string, а не union литералов. as const критичен для вывода union из массива констант.

11

Enums — почему многие их избегают?

Короткий ответ: Числовые enum генерируют рантайм-объект (нарушают type erasure), имеют небезопасное обратное отображение и неочевидное поведение. Часто предпочитают union литералов или as const объекты.

Подробно:

// Обычный enum компилируется в рантайм-объект (не стирается!)
enum Color { Red, Green, Blue } // Red=0, Green=1, Blue=2

// Числовой enum принимает любое число — небезопасно
let c: Color = 5; // ❌ логически неверно, но компилируется (до строгих версий)

// const enum — инлайнится, не оставляет рантайм-кода
const enum Size { S, M, L }
let s = Size.M; // компилируется в: let s = 1;

// Предпочтительная альтернатива — union литералов
type ColorU = "red" | "green" | "blue";

// Или объект as const (есть и тип, и значения для итерации)
const Color2 = { Red: "red", Green: "green", Blue: "blue" } as const;
type Color2 = typeof Color2[keyof typeof Color2]; // "red" | "green" | "blue"

Минусы enum: рантайм-вес, проблемы с tree-shaking, const enum несовместим с isolatedModules (Babel, esbuild), числовые enum небезопасны.

⚠️ Ловушка: const enum не работает при сборке через Babel/esbuild с isolatedModules, так как требует информации о типах для инлайна. Это частая причина отказа от него в современных проектах.

12

Generics: зачем нужны, функции, классы, constraints, default params

Короткий ответ: Дженерики — параметры типа, позволяющие писать переиспользуемый код, сохраняющий связь между типами входа и выхода без потери информации (в отличие от any).

Подробно:

// Дженерик-функция: связывает вход и выход
function identity<T>(value: T): T {
  return value;
}
const a = identity("hi"); // a: string (выведено)
const b = identity(42);   // b: number

// Constraints: ограничение через extends
function getLength<T extends { length: number }>(x: T): number {
  return x.length;
}
getLength("abc");      // ✅
getLength([1, 2, 3]);  // ✅
// getLength(42);      // ❌ number нет length

// keyof + дженерики — типобезопасный доступ к полю
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: "Ann", age: 30 };
const name = getProp(user, "name"); // string
// getProp(user, "foo"); // ❌ нет такого ключа

// Default type params
interface Box<T = string> {
  value: T;
}
const box: Box = { value: "default is string" };

// Дженерик-класс
class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}
const s = new Stack<number>();

⚠️ Ловушка: Не злоупотребляйте дженериками. Если параметр типа используется лишь в одном месте, он, возможно, лишний. И <T,> (с запятой) нужно в .tsx, чтобы отличить от JSX-тега.

13

Utility types: Partial, Required, Readonly, Pick, Omit, Record и др.

Короткий ответ: Встроенные утилитарные типы трансформируют существующие типы: делают поля опциональными/обязательными, выбирают/исключают ключи, строят словари, извлекают типы возврата/параметров.

Подробно:

interface User {
  id: number;
  name: string;
  email?: string;
}

// Partial<T> — все поля опциональны (для update)
type UserUpdate = Partial<User>; // { id?, name?, email? }

// Required<T> — все обязательны
type FullUser = Required<User>;  // email становится обязательным

// Readonly<T> — все только для чтения
type FrozenUser = Readonly<User>;

// Pick<T, K> — выбрать подмножество ключей
type UserPreview = Pick<User, "id" | "name">;

// Omit<T, K> — исключить ключи
type UserNoId = Omit<User, "id">; // { name, email? }

// Record<K, V> — словарь
type RolePermissions = Record<"admin" | "user", string[]>;
const perms: RolePermissions = { admin: ["all"], user: ["read"] };

// Exclude / Extract — фильтрация union
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T2 = Extract<"a" | "b" | "c", "a" | "z">; // "a"

// NonNullable — убрать null | undefined
type T3 = NonNullable<string | null | undefined>; // string

// ReturnType / Parameters — из типа функции
function makeUser(name: string, age: number) {
  return { name, age };
}
type MadeUser = ReturnType<typeof makeUser>;   // { name: string; age: number }
type MakeArgs = Parameters<typeof makeUser>;   // [string, number]

// Awaited — развернуть Promise
type Data = Awaited<Promise<Promise<number>>>; // number

⚠️ Ловушка: Omit не проверяет, что исключаемый ключ существует в типе (Omit<User, "typo"> не выдаст ошибку до TS 5.x в строгих режимах). И Pick/Omit теряют сигнатуры индекса и методы-перегрузки в некоторых случаях.

14

Type narrowing: typeof, instanceof, in, user-defined type guards

Короткий ответ: Сужение — процесс, в котором компилятор уточняет тип внутри ветки по проверке. Инструменты: typeof, instanceof, оператор in, проверки на truthiness/равенство и пользовательские type guards с предикатом x is T.

Подробно:

// typeof — для примитивов
function pad(x: string | number) {
  if (typeof x === "number") {
    return x.toFixed(2); // x: number
  }
  return x.trim();       // x: string
}

// instanceof — для классов
function handle(e: Error | string) {
  if (e instanceof Error) {
    return e.message; // e: Error
  }
  return e;            // e: string
}

// in — проверка наличия свойства
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // Fish
  } else {
    animal.fly();  // Bird
  }
}

// User-defined type guard: предикат x is T
function isString(x: unknown): x is string {
  return typeof x === "string";
}
function process(val: unknown) {
  if (isString(val)) {
    val.toUpperCase(); // val: string
  }
}

// Assertion function
function assertIsDefined<T>(x: T): asserts x is NonNullable<T> {
  if (x == null) throw new Error("not defined");
}

⚠️ Ловушка: Type guard с x is T доверяет вашей логике — компилятор не проверяет её корректность. Если предикат врёт (return true всегда), типобезопасность нарушается без ошибки. Также typeof null === "object" — классическая ловушка при сужении объектов.

15

Discriminated (tagged) unions и exhaustiveness check

Короткий ответ: Discriminated union — union объектов с общим литеральным полем-дискриминантом (kind/type). По нему компилятор сужает тип. С never в default достигается проверка полноты (exhaustiveness).

Подробно:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;   // сужено до circle
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default:
      // Exhaustiveness: если добавят новый kind и не обработают,
      // shape здесь не будет never → ошибка компиляции
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

Это один из самых мощных паттернов TS: моделирование состояний (loading/success/error), событий, AST.

type RequestState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; message: string };

function render(state: RequestState) {
  if (state.status === "success") {
    return state.data;     // доступ к data только здесь
  }
  // state.data; // ❌ в других ветках нет
}

⚠️ Ловушка: Дискриминант должен быть литеральным типом ("circle", не string). Если поле объявлено как string, сужение не сработает. И не забывайте про never-проверку — без неё забытая ветка проходит молча.

16

keyof, оператор typeof, indexed access types (T[K])

Короткий ответ: keyof T — union ключей типа. typeof value (в позиции типа) — извлекает тип из значения. T[K] — indexed access — тип значения по ключу.

Подробно:

interface User {
  id: number;
  name: string;
  active: boolean;
}

// keyof — union строковых литералов ключей
type UserKeys = keyof User; // "id" | "name" | "active"

// typeof — тип из значения
const config = { host: "localhost", port: 8080 };
type Config = typeof config; // { host: string; port: number }

// Indexed access — тип значения по ключу
type IdType = User["id"];           // number
type NameOrId = User["id" | "name"]; // number | string

// Комбинация: тип всех значений
type Values = User[keyof User]; // number | string | boolean

// Реальный кейс: тип элемента массива
const arr = [{ x: 1 }, { x: 2 }];
type Item = typeof arr[number]; // { x: number }
// Связка в дженериках — типобезопасный геттер
function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] {
  return items.map((item) => item[key]);
}
const names = pluck([{ name: "A" }, { name: "B" }], "name"); // string[]

⚠️ Ловушка: keyof для типа с индексной сигнатурой { [k: string]: number } даёт string | number (number из-за того, что числовые ключи приводятся к строкам). А typeof в позиции значения и typeof в позиции типа — разные операторы.

17

Mapped types

Короткий ответ: Mapped types создают новый тип, итерируясь по ключам существующего: { [K in keyof T]: ... }. На них построены Partial, Readonly, Record и т.д.

Подробно:

interface User {
  id: number;
  name: string;
}

// Базовый mapped type
type ReadonlyUser = { readonly [K in keyof User]: User[K] };

// Так реализован Partial
type MyPartial<T> = { [K in keyof T]?: T[K] };

// Модификаторы: добавить (+) / убрать (-) readonly и ?
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] }; // убрать optional

// Изменение типа значений
type Stringify<T> = { [K in keyof T]: string };
type S = Stringify<User>; // { id: string; name: string }

// Key remapping через `as` (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }

// Фильтрация ключей через `as ... never`
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

⚠️ Ловушка: Mapped type над union «гомоморфен» только при [K in keyof T] — тогда модификаторы и optional исходного типа сохраняются. При [K in SomeUnion] (не keyof) это свойство теряется. И ремаппинг ключей через as доступен только начиная с TS 4.1.

18

Conditional types и infer

Короткий ответ: Conditional type T extends U ? X : Y выбирает тип по условию. infer объявляет переменную типа внутри условия, чтобы «выловить» вложенный тип.

Подробно:

// Базовый conditional type
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"

// Distributive: над union применяется к каждому члену
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]

// infer — извлечь вложенный тип
type ElementType<T> = T extends (infer U)[] ? U : never;
type E = ElementType<number[]>; // number

// Так работает ReturnType
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Ret = MyReturnType<() => string>; // string

// Так работает Awaited (упрощённо)
type MyAwaited<T> = T extends Promise<infer V> ? MyAwaited<V> : T;
type W = MyAwaited<Promise<Promise<number>>>; // number

// Несколько infer
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F = FirstArg<(a: string, b: number) => void>; // string

⚠️ Ловушка: Distributive поведение срабатывает только когда проверяемый тип — «голый» параметр (T extends ...). Чтобы отключить дистрибутивность, оберните в кортеж: [T] extends [U] ? .... Это частый трюк, например, чтобы проверить «является ли T именно never».

19

Optional (?) и readonly модификаторы

Короткий ответ: ? делает свойство/параметр необязательным (может отсутствовать, тип становится T | undefined). readonly запрещает переприсваивание свойства после инициализации (только на уровне типов).

Подробно:

interface Config {
  host: string;
  port?: number;          // optional → number | undefined
  readonly apiKey: string; // нельзя менять после создания
}

const c: Config = { host: "localhost", apiKey: "secret" };
// c.apiKey = "new"; // ❌ Cannot assign to 'apiKey', it is read-only
c.port = 8080; // ✅

// Опциональные параметры функции
function greet(name: string, greeting?: string) {
  return `${greeting ?? "Hi"}, ${name}`;
}

// readonly для массивов и кортежей
const nums: readonly number[] = [1, 2, 3];
// nums.push(4); // ❌ push отсутствует у readonly array

// ReadonlyArray<T> — эквивалент
const list: ReadonlyArray<string> = ["a"];

⚠️ Ловушка: readonly — только compile-time. В рантайме объект можно изменить (через any, или из JS). Также readonly поверхностный: readonly { nested: { x: number } } не защищает nested.x. Для глубокой неизменяемости нужны as const или DeepReadonly-утилиты.

20

Tuple types

Короткий ответ: Кортеж — массив фиксированной длины с известным типом каждого элемента по позиции. Поддерживает опциональные, rest- и именованные элементы.

Подробно:

// Базовый кортеж
let point: [number, number] = [10, 20];
let entry: [string, number] = ["age", 30];

// Именованные элементы (только для читаемости)
type Range = [start: number, end: number];

// Опциональные и rest
type Args = [string, number?];            // второй необязателен
type Variadic = [string, ...number[]];    // строка + любое число чисел

// readonly tuple
const rgb: readonly [number, number, number] = [255, 0, 0];

// Деструктуризация сохраняет типы
const [name, age]: [string, number] = ["Ann", 30];

// Реальный кейс: возврат пары (как useState)
function useToggle(): [boolean, () => void] {
  let state = false;
  return [state, () => { state = !state; }];
}
const [on, toggle] = useToggle();

⚠️ Ловушка: Без as const массив-литерал выводится как T[], а не кортеж. const pair = [1, "a"](number | string)[], не [number, string]. Для кортежа нужно явно аннотировать тип или использовать as const.

21

Function types и overloads

Короткий ответ: Тип функции описывает параметры и возврат: (a: number) => string. Перегрузки (overloads) позволяют объявить несколько сигнатур для одной функции с разным поведением по типам аргументов.

Подробно:

// Тип функции
type BinaryOp = (a: number, b: number) => number;
const add: BinaryOp = (a, b) => a + b;

// Тип с опциональным/rest параметром
type Logger = (msg: string, ...meta: unknown[]) => void;

// Перегрузки: несколько сигнатур + одна реализация
function parse(value: string): string[];
function parse(value: number): number;
function parse(value: string | number): string[] | number {
  if (typeof value === "string") return value.split(",");
  return value * 2;
}
const a = parse("a,b"); // string[]
const b = parse(10);    // number

// Сигнатура вызова + свойства (callable объект)
interface Counter {
  (): number;       // вызываемый
  count: number;    // и со свойством
  reset(): void;
}

⚠️ Ловушка: Сигнатура реализации перегрузки не видна снаружи — она только для тела функции. Вызывать можно лишь по объявленным перегрузкам. Часто перегрузки можно заменить дженериком или union-типом, что проще в поддержке.

22

Type assertions (as) и non-null assertion (!) — когда опасно

Короткий ответ: as T говорит компилятору «доверься, это тип T» без проверки. ! (non-null assertion) убирает null | undefined. Оба отключают проверки и опасны — могут скрыть рантайм-ошибку.

Подробно:

// Type assertion
const input = document.getElementById("x") as HTMLInputElement;
input.value; // компилятор верит, но в рантайме может быть null

// as опасен — позволяет «солгать»
const x = "hello" as unknown as number; // двойной каст обходит защиту

// Non-null assertion
function getLength(s?: string) {
  return s!.length; // утверждаем, что s не undefined — упадёт, если undefined
}

// Более безопасные альтернативы:
const el = document.getElementById("x");
if (el instanceof HTMLInputElement) {
  el.value; // ✅ сужение, а не каст
}

const len = s?.length ?? 0; // ✅ optional chaining вместо !

as оправдан когда:

  • Вы знаете больше компилятора (DOM, внешние данные после валидации).
  • as const — безопасное использование.

⚠️ Ловушка: as и ! не делают рантайм-преобразований — они только успокаивают компилятор. Если данные не соответствуют, ошибка проявится позже и в неожиданном месте. Двойной каст as unknown as T — красный флаг: почти всегда означает скрытую проблему типизации.

23

Declaration files (.d.ts), @types, DefinitelyTyped

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

Подробно:

// math.d.ts — описание типов для math.js без типов
declare module "legacy-math" {
  export function add(a: number, b: number): number;
  export const PI: number;
}

// Глобальные объявления
declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}

// Объявление переменной/функции (ambient)
declare const __VERSION__: string;
declare function gtag(...args: unknown[]): void;

Как это работает:

  • Многие npm-пакеты не содержат типов. Сообщество публикует типы в DefinitelyTyped → они доступны как @types/имя (npm i -D @types/node @types/react).
  • TS автоматически подхватывает @types/* из node_modules/@types.
  • Современные библиотеки часто поставляют .d.ts прямо в пакете (поле types/typings в package.json).
npm install --save-dev @types/lodash

⚠️ Ловушка: Версия @types/lib должна примерно соответствовать версии самой библиотеки — рассинхрон даёт неверные или отсутствующие типы. И .d.ts описывает контракт, но не гарантирует его: если реальный JS отличается от объявления, компилятор этого не заметит.

24

tsconfig: strict, noImplicitAny, strictNullChecks

Короткий ответ: tsconfig.json настраивает компилятор. strict: true включает набор строгих проверок (включая noImplicitAny и strictNullChecks) — рекомендуемая база для новых проектов.

Подробно:

{
  "compilerOptions": {
    "strict": true,              // включает все строгие флаги
    "noImplicitAny": true,       // запретить неявный any
    "strictNullChecks": true,    // null/undefined в типах явно
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,        // не проверять .d.ts зависимостей (скорость)
    "noUncheckedIndexedAccess": true // arr[i] → T | undefined
  }
}

Что дают ключевые флаги:

// noImplicitAny: параметр без типа → ошибка
function f(x) {} // ❌ 'x' implicitly has type 'any'

// strictNullChecks: null/undefined не присваиваются куда попало
let s: string = null; // ❌ при strictNullChecks
let s2: string | null = null; // ✅ нужно явно

function find(): string | undefined { return undefined; }
const r = find();
// r.length; // ❌ Object is possibly 'undefined'

strict включает также: strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict.

⚠️ Ловушка: Без strictNullChecks система типов «лжёт» — null/undefined присваиваются любому типу, и Object is possibly undefined не ловится. Это самый ценный флаг; включать strict стоит с самого старта, иначе миграция legacy-кода болезненна.

25

Типизация в React: props, события, useState, useRef

Короткий ответ: Props типизируют через type/interface. Хуки дженерики: useState<T>, useRef<T>. События — типы из React (React.ChangeEvent, React.MouseEvent). React.FC сегодня чаще избегают в пользу явной типизации props.

Подробно:

// Props — предпочтительно явная типизация, без FC
interface ButtonProps {
  label: string;
  variant?: "primary" | "secondary";
  onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
  children?: React.ReactNode;
}

function Button({ label, variant = "primary", onClick }: ButtonProps) {
  return <button className={variant} onClick={onClick}>{label}</button>;
}

// useState с явным типом, когда вывод недостаточен
const [count, setCount] = useState(0);            // выведет number
const [user, setUser] = useState<User | null>(null); // нужно явно

// События
function Input() {
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
  return <input onChange={onChange} />;
}

// useRef — для DOM и мутабельных значений
const inputRef = useRef<HTMLInputElement>(null); // ref на DOM
const timerRef = useRef<number | undefined>(undefined); // мутабельное значение

// Дженерик-компонент
function List<T>({ items, render }: { items: T[]; render: (item: T) => React.ReactNode }) {
  return <>{items.map(render)}</>;
}

⚠️ Ловушка: React.FC неявно добавлял children (до React 18 в типах) и плохо работает с дженерик-компонентами, поэтому многие команды от него отказались. С useRef<T>(null) для DOM ref свойство .current будет T | null — нужна проверка перед доступом.

26

Generics в реальных задачах: типобезопасный API-клиент

Короткий ответ: Дженерики позволяют построить API-клиент, где тип ответа выводится из эндпоинта/входных данных, а не задаётся any. Связка keyof, extends, conditional types даёт полную типобезопасность.

Подробно:

// Карта эндпоинтов → типы ответов
interface ApiRoutes {
  "/users": { id: number; name: string }[];
  "/users/:id": { id: number; name: string; email: string };
  "/posts": { id: number; title: string }[];
}

// Типобезопасный fetch: тип ответа выводится из пути
async function apiGet<Path extends keyof ApiRoutes>(
  path: Path
): Promise<ApiRoutes[Path]> {
  const res = await fetch(path);
  return res.json() as Promise<ApiRoutes[Path]>;
}

const users = await apiGet("/users");      // { id; name }[]
const user = await apiGet("/users/:id");   // { id; name; email }
// await apiGet("/unknown");               // ❌ нет такого пути
// Дженерик-обёртка результата с discriminated union
type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

async function safeGet<P extends keyof ApiRoutes>(
  path: P
): Promise<ApiResult<ApiRoutes[P]>> {
  try {
    const data = await apiGet(path);
    return { ok: true, data };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e.message : "unknown" };
  }
}

const result = await safeGet("/users");
if (result.ok) {
  result.data.length; // ✅ тип известен
}

⚠️ Ловушка: res.json() возвращает any (по-настоящему Promise<any>), поэтому ваш as Promise<ApiRoutes[Path]> — это обещание, а не проверка. Рантайм-данные могут не совпасть с типом. Для настоящей безопасности на границе используйте валидацию схемой (zod) и выводите тип из неё.

27

Зачем TypeScript, если есть тесты?

Короткий ответ: Тесты и типы решают разные задачи и дополняют друг друга. Типы проверяют структурную корректность непрерывно и исчерпывающе во всём коде; тесты проверяют поведение в конкретных сценариях. Типы — дешёвая первая линия защиты.

Подробно:

Сравнение:

  • Покрытие. Типы проверяют каждый вызов и доступ к свойству автоматически. Тесты — только то, что вы написали. Типы дают «100% покрытие» против опечаток и неверных аргументов бесплатно.
  • Стоимость. Типы пишутся один раз в сигнатуре; защищают все вызовы. Тесты надо писать и поддерживать для каждого случая.
  • Скорость обратной связи. Типы — мгновенно в IDE. Тесты — после запуска.
  • Класс ошибок. Типы ловят «неправильную форму данных» (передал не то, обратился к несуществующему полю). Тесты ловят «неправильную логику» (формула посчитала не так).
// Тест проверит, что calculateTotal([1,2]) === 3
// Тип гарантирует, что НИКТО не вызовет calculateTotal("abc")
function calculateTotal(prices: number[]): number {
  return prices.reduce((a, b) => a + b, 0);
}

Вывод: типы не заменяют тесты (они не проверяют, что бизнес-логика верна), а тесты не заменяют типы (они не покрывают все возможные неверные использования). Лучшая практика — и то, и другое.

⚠️ Ловушка: Типы дают ложное чувство безопасности на границах системы. Тип User не гарантирует, что API вернул User — типы стираются и не валидируют рантайм-данные. Здесь как раз нужны и тесты, и рантайм-валидация.

Источники

Источники и редакционная политика

Материалы RecallDeck сопоставлены с официальной документацией и открытыми публикациями компаний, когда первичный источник доступен. Мы не связаны с упомянутыми работодателями, не публикуем конфиденциальные задания и не продаём места в подборках. Формат найма может меняться — уточняйте его у рекрутера.

От чтения к воспроизведению

Отрепетируйте полный цикл интервью.

RecallDeck возвращает сложные темы по расписанию и помогает удерживать в памяти язык, SQL, архитектуру и поведенческие истории.

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

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

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

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

RSS