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

Страницы:  1

Ответить
 

Professor Seleznov


Собеседования по TypeScript всё чаще проверяют не только знание синтаксиса, но и умение видеть «узкие места» в уже работающем коде. Задача кандидата - не просто сказать «тут ошибка», а предложить более безопасное, читаемое и поддерживаемое решение.
В этой статье собраны практические вопросы, основанные на реальных принципах рефакторинга TypeScript. Каждый пример показывает типичный код, который можно улучшить, и задаёт направление для размышлений.
pic
-
🔹 Типы и сужение типов
Вопрос:
«У нас есть код, где 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'; // и так понятно, что это string
const items = new Map<string, number>([['x', 1]]); // тип и так выведется
const [loaded, setLoaded] = useState<boolean>(false); // boolean очевиден

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

// Достаточно ✅
const ROLE = 'admin'; // выводится как 'admin' (литерал), а не просто string
const 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-ignore
const result = legacyFunc('test');

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

// ✅ @ts-expect-error — если ошибка уйдёт, компилятор предупредит, что комментарий лишний
// @ts-expect-error: legacyFunc принимает число, а не строку — поправим в v2
const 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-синтаксис читается лучше, особенно с readonly
const 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 запросов/мин
// См. тикет #4421
const 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 импортов
  • Новый разработчик быстро находит, где что
-Источник
 
Loading...
Error