Связывайте ответ с браузером, доступностью, производительностью и пользовательским состоянием — именно там видна зрелость frontend-инженера.
Вопросы и ответы
27 подробных ответов
01Зачем нужен TypeScript? Что он даёт поверх JavaScript?
junior
Короткий ответ: 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)?
concept
Короткий ответ: Компилятор 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)? Чем отличается от номинальной?
middle
Короткий ответ: В 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
junior
Короткий ответ: Примитивы (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.
05any vs unknown vs never — глубоко, когда что использовать
senior
Короткий ответ: 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) ....
06any vs unknown — почему unknown безопаснее?
concept
Короткий ответ: 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 в цепочке может «съесть» типобезопасность большого участка кода без единой ошибки компиляции.
07Type vs Interface — различия, когда что использовать
middle
Короткий ответ: Оба описывают форму объекта и почти взаимозаменяемы. 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 на практике?
concept
Короткий ответ: На практике для описания объектов разница минимальна — выбирайте по соглашению команды. Реально важны три момента: 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.
09Union (|) и Intersection (&) типы
middle
Короткий ответ: 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 объектов = больше требований (меньше подходящих значений).
10Literal types и const assertions (as const)
middle
Короткий ответ: Литеральные типы — конкретное значение как тип ("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 из массива констант.
11Enums — почему многие их избегают?
middle
Короткий ответ: Числовые 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, так как требует информации о типах для инлайна. Это частая причина отказа от него в современных проектах.
12Generics: зачем нужны, функции, классы, constraints, default params
middle
Короткий ответ: Дженерики — параметры типа, позволяющие писать переиспользуемый код, сохраняющий связь между типами входа и выхода без потери информации (в отличие от 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-тега.
13Utility types: Partial, Required, Readonly, Pick, Omit, Record и др.
senior
Короткий ответ: Встроенные утилитарные типы трансформируют существующие типы: делают поля опциональными/обязательными, выбирают/исключают ключи, строят словари, извлекают типы возврата/параметров.
Подробно:
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 теряют сигнатуры индекса и методы-перегрузки в некоторых случаях.
14Type narrowing: typeof, instanceof, in, user-defined type guards
middle
Короткий ответ: Сужение — процесс, в котором компилятор уточняет тип внутри ветки по проверке. Инструменты: 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" — классическая ловушка при сужении объектов.
15Discriminated (tagged) unions и exhaustiveness check
senior
Короткий ответ: 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-проверку — без неё забытая ветка проходит молча.
16keyof, оператор typeof, indexed access types (T[K])
senior
Короткий ответ: 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 в позиции типа — разные операторы.
17Mapped types
senior
Короткий ответ: 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.
18Conditional types и infer
senior
Короткий ответ: 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».
19Optional (?) и readonly модификаторы
junior
Короткий ответ: ? делает свойство/параметр необязательным (может отсутствовать, тип становится 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-утилиты.
20Tuple types
middle
Короткий ответ: Кортеж — массив фиксированной длины с известным типом каждого элемента по позиции. Поддерживает опциональные, 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.
21Function types и overloads
middle
Короткий ответ: Тип функции описывает параметры и возврат: (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-типом, что проще в поддержке.
22Type assertions (as) и non-null assertion (!) — когда опасно
middle
Короткий ответ: 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 — красный флаг: почти всегда означает скрытую проблему типизации.
23Declaration files (.d.ts), @types, DefinitelyTyped
middle
Короткий ответ: .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 отличается от объявления, компилятор этого не заметит.
24tsconfig: strict, noImplicitAny, strictNullChecks
middle
Короткий ответ: 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
middle
Короткий ответ: 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 — нужна проверка перед доступом.
26Generics в реальных задачах: типобезопасный API-клиент
senior
Короткий ответ: Дженерики позволяют построить 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, если есть тесты?
concept
Короткий ответ: Тесты и типы решают разные задачи и дополняют друг друга. Типы проверяют структурную корректность непрерывно и исчерпывающе во всём коде; тесты проверяют поведение в конкретных сценариях. Типы — дешёвая первая линия защиты.
Подробно:
Сравнение:
- Покрытие. Типы проверяют каждый вызов и доступ к свойству автоматически. Тесты — только то, что вы написали. Типы дают «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, архитектуру и поведенческие истории.