|
Professor Seleznov
|
Собеседования по 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'; // и так понятно, что это 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 импортов
- Новый разработчик быстро находит, где что
-Источник
|