Вопросы на собеседование: Рефакторинг TypeScript

от автора

Собеседования по TypeScript всё чаще проверяют не только знание синтаксиса, но и умение видеть «узкие места» в уже работающем коде. Задача кандидата — не просто сказать «тут ошибка», а предложить более безопасное, читаемое и поддерживаемое решение.

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


🔹 Типы и сужение типов

Вопрос:

«У нас есть код, где TypeScript выводит слишком широкий тип. Как это исправить и зачем это нужно?»

// Было ❌const users = new Map(); // Map<any, any> — слишком широкоusers.set('anna', 28);type Status = 'active' | 'blocked';const [status, setStatus] = useState('active'); // выводится как string
Скрытый текст
// Стало ✅const users = new Map<string, number>(); // чётко: ключ — строка, значение — числоusers.set('anna', 28);type Status = 'active' | 'blocked';const [status, setStatus] = useState<Status>('active'); // сужаем до нужных значений

Зачем:

  • Ошибки ловятся на этапе компиляции, а не в продакшене

  • Код становится понятнее: сразу видно, какие данные допустимы

  • Легче рефакторить: если поменять тип — компилятор покажет все места, где нужно поправить


🔹 Не дублируйте то, что и так очевидно

Вопрос:

«Когда стоит явно указывать тип переменной, а когда можно довериться выводу типов?»

// Избыточно ❌const role: string = 'admin'; // и так понятно, что это stringconst items = new Map<string, number>([['x', 1]]); // тип и так выведетсяconst [loaded, setLoaded] = useState<boolean>(false); // boolean очевиден
Скрытый текст
// Достаточно ✅const ROLE = 'admin'; // выводится как 'admin' (литерал), а не просто stringconst items = new Map([['x', 1]]); // выводится Map<string, number>const [loaded, setLoaded] = useState(false); // выводится boolean

Правило: Указывайте тип явно только тогда, когда это помогает сузить тип. Во всех остальных случаях — доверяйте TypeScript.


🔹 Неизменяемость данных

Вопрос:

«Как защитить данные от случайных изменений в функции?»

// Опасно ❌ — можно случайно изменить исходной массивconst removeFirst = (list: Array<User>) => {  if (list.length === 0) return list;  return list.splice(1); // меняет исходной массив!};
Скрытый текст
// Безопасно ✅const removeFirst = (list: ReadonlyArray<User>)

Почему важно:

  • Меньше багов из-за побочных эффектов

  • Легче тестировать и отлаживать


🔹 Обязательные и опциональные поля

Вопрос:

«Почему не стоит делать все поля в интерфейсе опциональными?»

// Сложно и небезопасно ❌type Account = {  id?: number;  email?: string;  isAdmin?: boolean;  permissions?: string[];  plan?: 'free' | 'pro';};// При использовании придётся постоянно писать account?.email?.toLowerCase()...
Скрытый текст
// Чётко и безопасно ✅ — через объединение с дискриминаторомtype AdminAccount = {  role: 'admin';  id: number;  email: string;  permissions: readonly string[];};type GuestAccount = {  role: 'guest';  tempToken: string;};type Account = AdminAccount | GuestAccount; // компилятор знает, какие поля где есть

Плюсы:

  • Ясно, какие данные обязательны в каждом сценарии

  • Нет лишнего ?. в коде

  • Ошибки «поля не существует» ловятся сразу


🔹 Дискриминируемые объединения (Discriminated Unions)

Вопрос:

«Что это и зачем нужно? Покажите на примере рефакторинга.»

// Было: много флагов — запутанно ❌type Request = {  isLoading?: boolean;  isError?: boolean;  data?: any;  error?: string;};
Скрытый текст
// Стало: один явный статус — понятно ✅type RequestSuccess = { status: 'success'; data: Product[] };type RequestLoading = { status: 'loading' };type RequestError = { status: 'error'; message: string };type Request = RequestSuccess | RequestLoading | RequestError;// Теперь компилятор проверит, что вы обработали все случаи:const render = (req: Request) => {  switch (req.status) {    case 'success': return <List items={req.data} />; // req.data точно есть    case 'loading': return <Spinner />;    case 'error': return <Error msg={req.message} />; // req.message точно есть    // забыли случай? — компилятор предупредит!  }};

Зачем:

  • Убирает неопределённость: в каждом состоянии известны точные поля

  • Exhaustiveness check: если добавили новый статус — компилятор заставит обработать его везде

  • Код самодокументируется


🔹 as const satisfies — мощный инструмент для констант

Вопрос:

«Как безопасно описать набор констант, чтобы и типы проверялись, и значения не менялись?»

type Role = 'viewer' | 'editor' | 'admin';// ❌ Широкий типconst ROLES: readonly Role[] = ['viewer', 'editor'];// ❌ as const без проверки — можно опечататься в значенияхconst ROLES = ['viwer', 'edior'] as const; // опечатки не заметим!
Скрытый текст
// ✅ Идеально: и значения «заморожены», и типы провереныconst ROLES = ['viewer', 'editor', 'admin'] as const satisfies readonly Role[];

Что даёт:

  • as const — делает значения readonly и сужает типы до литералов

  • satisfies — проверяет, что значения соответствуют ожидаемому типу, но не «расширяет» их

  • Вместе — максимальная безопасность без потери удобства


🔹 Шаблонные строковые типы (Template Literal Types)

Вопрос:

«Как защититься от опечаток в строках вроде путей API или ключей перевода?»

// Было ❌ — опечатка в строке = ошибка в рантаймеconst endpoint = '/api/usrers'; // "usrers" вместо "users" — компилятор молчит
Скрытый текст
// Стало ✅ — только допустимые значенияtype ApiPath = 'users' | 'posts' | 'comments';type ApiEndpoint = `/api/${ApiPath}`; // "/api/users" | "/api/posts" | "/api/comments"const endpoint: ApiEndpoint = '/api/users'; // ✅const bad: ApiEndpoint = '/api/usrers'; // ❌ Ошибка компиляции!

Где ещё полезно:

  • Ключи локализации: translation.${Page}.${Key}

  • CSS-классы: ${Color}-${Shade} => 'blue-400' | 'red-200'

  • SQL-запросы: SELECT ${Column} FROM ${Table}


🔹 any vs unknown

Вопрос:

«В чём разница и почему any — это зло?»

// ❌ any отключает проверку типов — ошибки пролезут в продакшенconst data: any = apiCall();const name: string = data.userName; // компилятор молчит, даже если userName нет
Скрытый текст
// ✅ unknown требует явной проверки перед использованиемconst data: unknown = apiCall();// Вариант 1: тип-гардif (typeof data === 'object' && data !== null && 'userName' in data) {  const name = (data as { userName: string }).userName;}// Вариант 2: утверждение типа (только если уверены!)const name = (data as { userName: string }).userName;

Правило: unknown — безопасная альтернатива any. Всегда сужайте тип перед использованием.


🔹 Утверждения типов: когда можно, а когда нет

Вопрос:

«Можно ли использовать as User или user!.name

type User = { name: string; avatar: string | null };// ❌ Опасно: компилятор верит вам на слово, но в рантайме может упастьconst user = { name: 'Misha' } as User; // avatar отсутствует!render(user!.avatar); // Runtime error: cannot read property of null
Скрытый текст
// ✅ Безопасно: проверяем перед использованиемif (user.avatar !== null) {  render(user.avatar);}

Когда допустимо:

  • Работаете с чужой библиотекой, где типы неточные

  • Только после явной проверки (type guard)

  • С комментарием, почему это необходимо


🔹 @ts-expect-error vs @ts-ignore

Вопрос:

«Как правильно заглушить ошибку TypeScript, если иначе нельзя?»

// ❌ @ts-ignore — ошибка может «исчезнуть» после рефакторинга, а комментарий останется// @ts-ignoreconst result = legacyFunc('test');
Скрытый текст
// ✅ @ts-expect-error — если ошибка уйдёт, компилятор предупредит, что комментарий лишний// @ts-expect-error: legacyFunc принимает число, а не строку — поправим в v2const result = legacyFunc('test');

Почему важно: @ts-expect-error — самодокументирующийся и самопроверяющийся. Не даёт забыть про технический долг.


🔹 type vs interface

Вопрос:

«Что выбрать и почему?»

// ❌ interface не умеет в объединения типовinterface Status = 'ok' | 'error'; // Ошибка!
Скрытый текст
// ✅ type — универсальнееtype Status = 'ok' | 'error';type User = { name: string; status: Status };

Правило: Используйте type по умолчанию. interface— только когда нужно расширение (declaration merging), например, для глобальных типов.


🔹 Массивы: T[] vs Array

Вопрос:

«Есть ли разница и что лучше?»

// ❌ Синтаксис T[] может запутать в сложных случаяхconst list: readonly string[] = ['a', 'b'];
Скрытый текст
// ✅ Generic-синтаксис читается лучше, особенно с readonlyconst list: ReadonlyArray<string> = ['a', 'b'];const matrix: Array<Array<number>> = [[1, 2], [3, 4]];

Плюсы Array<T>:

  • Однообразный стиль во всём проекте

  • Легче читать вложенные типы

  • Явно видно, что это массив, а не что-то другое


🔹 Импорт типов: import type

Вопрос:

«Зачем отдельно импортировать типы, если можно просто import { X }

// ❌ Может попасть в бандл лишний кодimport { User } from './types'; // User — только тип, но бандлер может включить файл
Скрытый текст
// ✅ Чётко: это только тип, в рантайме не нужноimport type { User } from './types';

Зачем:

  • Уменьшает размер бандла (tree-shaking)

  • Явно разделяет «код» и «типы»

  • Помогает избежать циклических зависимостей


🔹 Функции: один объект вместо кучи аргументов

Вопрос:

«Как сделать функцию удобной для расширения?»

// ❌ Непонятно, что за параметры и в каком порядкеcreateOrder('client', false, 60, 120, null, true, 2000);
Скрытый текст
// ✅ Объект с понятными ключами — легко читать и менятьcreateOrder({  method: 'client',  validate: false,  minLines: 60,  maxLines: 120,  default: null,  log: true,  timeout: 2000,});

Бонус: Можно добавить as const satisfies для проверки допустимых значений параметров.


🔹 Возвращаемые типы функций

Вопрос:

«Стоит ли всегда указывать возвращаемый тип?»

Скрытый текст

Правило:

  • ✅ В публичных API, хуках, утилитах — указывайте явно (защита от случайных изменений)

  • ⚪ Во внутренней логике — можно довериться выводу типов, если код простой

// Публичный хук — тип важенexport const useUsers = (): { users: User[]; loading: boolean } => { ... }// Внутренняя вспомогательная функция — вывод типов окconst formatName = (user: User) => `${user.firstName} ${user.lastName}`;

🔹 Флаги boolean vs объединение типов

Вопрос:

«Почему лучше один статус, чем пять булевых флагов?»

// ❌ Флаги накапливаются, состояния становятся неоднозначнымиconst isPending = true;const isProcessing = false;const isConfirmed = false;// А если все false? А если два true одновременно?
Скрытый текст
// ✅ Одно поле — одно значение — никаких сомненийtype OrderState = 'pending' | 'processing' | 'confirmed' | 'cancelled';const state: OrderState = 'pending';

Результат:

  • Невозможно невалидное состояние

  • Компоненты рендерятся через switch — компилятор проверит полноту

  • Легче добавлять новые статусы


🔹 null vs undefined

Вопрос:

«В чём разница и когда что использовать?»

Скрытый текст

Простое правило:

  • null — значение есть, но оно «пустое» (например, аватарка не загружена)

  • undefined — значения нет вообще (поле не передано, не инициализировано)

type Profile = {  avatar: string | null; // может быть пустым, но поле всегда есть  bio?: string; // может вообще отсутствовать в объекте};

Зачем: Чёткая семантика помогает избегать багов и делает код самодокументирующимся.


🔹 Именование: простые правила, которые спасают

Вопрос:

«Как называть переменные, функции и типы, чтобы код читался как книга?»

Скрытый текст

Что

Стиль

Пример

Переменные, функции

camelCase

userList, formatPrice

Булевы

с префиксом is/has

isActive, hasPermission

Константы

UPPER_CASE

MAX_RETRIES = 3

Типы, интерфейсы

PascalCase

UserProfile, ApiResponse

Дженерики

T + описание

TData, TResponse (не просто T)

Компоненты React / Vue

PascalCase

ProductCard, CheckoutForm

Пропсы-события

on*

onSubmit, onError

Обработчики

handle*

handleSubmit, handleError

Акронимы: ApiUrl, не APIURL; FaqList, не FAQList.


🔹 Комментарии: когда они нужны?

Вопрос:

«Стоит ли комментировать каждую строку?»

Скрытый текст

Правило:

  • ❌ Не пишите, что делает код — это должно быть видно из имён и структуры

  • ✅ Пишите, почему сделано именно так — если причина неочевидна

// ❌ Бесполезно// Умножаем на 60, чтобы получить минутыconst minutes = seconds * 60;// ✅ Полезно// Используем кэширование, потому что API лимитирует 100 запросов/мин// См. тикет #4421const data = await fetchWithCache(endpoint);

TSDoc: Используйте для публичных API, хуков, утилит — чтобы IDE показывала подсказки.


🔹 Структура проекта: где что хранить?

Вопрос:

«Как организовать файлы, чтобы рефакторинг не превращался в квест?»

Скрытый текст
 modules/ └──  ProductPage/     ├──  index.tsx          # Точка входа     ├──  components/        # Только для этой страницы     │   ├── ProductCard/     │   └── PriceBadge/     ├──  hooks/             # useProductData, useWishlist     ├──  utils/             # formatProductPrice     └──  api/               # fetchProduct, updateCart

Принцип: Группируйте по фичам, а не по типам файлов.

Импорт:

  • ./Component — если файл рядом

  • @/modules/ProductPage/utils — если из другого модуля

Почему:

  • Удалили фичу — удалили папку, и всё

  • Перенесли фичу — не надо править 20 импортов

  • Новый разработчик быстро находит, где что

ссылка на оригинал статьи https://habr.com/ru/articles/1033686/