63. Best practices
TypeScript: Best Practices для создания надежного и масштабируемого кода
Заголовок раздела «TypeScript: Best Practices для создания надежного и масштабируемого кода»
TypeScript, в основе своей, призван помочь разработчикам создавать более надежный и предсказуемый код. Однако, чтобы полностью раскрыть его потенциал, важно следовать определенным “лучшим практикам”. Этот урок погрузит вас в фундаментальные и продвинутые техники, которые сделают ваш TypeScript-код более безопасным, читаемым и легко поддерживаемым.
1. Проблема: Дикий запад JavaScript (Без типов)
Заголовок раздела «1. Проблема: Дикий запад JavaScript (Без типов)»В чистом JavaScript легко столкнуться с ошибками во время выполнения из-за неверных предположений о типах данных. Отсутствие статической проверки делает рефакторинг рискованным, а отладку — трудоемкой.
Рассмотрим простой пример обработки пользовательских данных:
function processUserData(data) { // Что, если 'data' не объект? Или не имеет 'name' или 'email'? if (data && typeof data.name === 'string' && typeof data.email === 'string') { const formattedName = data.name.trim(); const domain = data.email.split('@')[1]; console.log(`User: ${formattedName}, Domain: ${domain}`); return { success: true, user: { name: formattedName, domain } }; } else { console.error("Invalid user data provided."); return { success: false, error: "Invalid data" }; }}
processUserData({ name: "Bob" }); // Ошибка: email is undefined при вызове split()processUserData("Charlie"); // Ошибка: data.name is undefinedЭтот код изобилует ручными проверками, которые легко пропустить, и он все еще подвержен ошибкам.
2. Решение: Фундаментальная типобезопасность с TypeScript
Заголовок раздела «2. Решение: Фундаментальная типобезопасность с TypeScript»TypeScript позволяет нам формализовать ожидания от данных, перемещая многие проверки в этап компиляции.
interface UserInput { name: string; email: string;}
interface ProcessedUser { name: string; domain: string;}
interface ProcessingResult { success: boolean; user?: ProcessedUser; error?: string;}
function processUserData(data: UserInput): ProcessingResult { // TypeScript уже гарантирует, что data имеет name и email типа string. // Нет необходимости в runtime проверках 'typeof data.name === 'string'' const formattedName = data.name.trim(); const domain = data.email.split('@')[1]; // Здесь все еще возможен runtime-риск, если email невалиден // но это уже другая проблема - валидация данных, а не типизация.
return { success: true, user: { name: formattedName, domain } };}
// Теперь ошибки обнаруживаются на этапе компиляции:// processUserData({ name: "Bob" }); // Ошибка компиляции: Property 'email' is missing// processUserData("Charlie"); // Ошибка компиляции: Argument of type 'string' is not assignable3. Продвинутые техники и лучшие практики
Заголовок раздела «3. Продвинутые техники и лучшие практики»3.1. Строгий режим компилятора (strict: true)
Заголовок раздела «3.1. Строгий режим компилятора (strict: true)»Включение strict: true в вашем tsconfig.json активирует набор важных проверок, таких как noImplicitAny, strictNullChecks, strictFunctionTypes и другие. Это наиболее важная практика для надежного кода.
{ "compilerOptions": { "target": "es2020", "module": "commonjs", "strict": true, // Это включает множество полезных проверок "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }}3.2. Используйте const и readonly
Заголовок раздела «3.2. Используйте const и readonly»Для данных, которые не должны изменяться, используйте const для переменных и readonly для свойств объектов или элементов кортежей.
type Point = readonly [number, number]; // Кортеж только для чтения
const origin: Point = [0, 0];// origin[0] = 1; // Ошибка компиляции: Cannot assign to '0' because it is a read-only property.
interface Config { readonly apiUrl: string; timeout: number;}
const appConfig: Config = { apiUrl: "https://api.example.com", timeout: 5000,};
// appConfig.apiUrl = "https://new.api"; // Ошибка компиляцииappConfig.timeout = 10000; // OK3.3. Объединения (Union Types) и Дискриминирующие объединения (Discriminated Unions)
Заголовок раздела «3.3. Объединения (Union Types) и Дискриминирующие объединения (Discriminated Unions)»Для обработки данных, которые могут иметь несколько форм, используйте объединения. Дискриминирующие объединения — мощный паттерн для безопасной работы с ними.
interface SuccessResponse { status: "success"; data: any;}
interface ErrorResponse { status: "error"; message: string; errorCode: number;}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) { if (response.status === "success") { // TypeScript сужает тип до SuccessResponse console.log("Data received:", response.data); } else { // TypeScript сужает тип до ErrorResponse console.error(`Error ${response.errorCode}: ${response.message}`); }}
handleResponse({ status: "success", data: { user: "Alice" } });handleResponse({ status: "error", message: "Not found", errorCode: 404 });// handleResponse({ status: "pending" }); // Ошибка компиляции: Type '"pending"' is not assignable to type '"success" | "error"'3.4. Утилитарные типы (Utility Types)
Заголовок раздела «3.4. Утилитарные типы (Utility Types)»TypeScript предоставляет набор встроенных утилитарных типов для общих преобразований типов.
interface UserProfile { id: string; name: string; email: string; isActive: boolean;}
// Partial<T>: делает все свойства T необязательнымиtype PartialUserProfile = Partial<UserProfile>;// { id?: string; name?: string; email?: string; isActive?: boolean; }
// Pick<T, K>: выбирает набор свойств K из Ttype UserBasicInfo = Pick<UserProfile, "name" | "email">;// { name: string; email: string; }
// Omit<T, K>: исключает набор свойств K из Ttype UserSensitiveInfo = Omit<UserProfile, "name" | "email">;// { id: string; isActive: boolean; }
// Readonly<T>: делает все свойства T только для чтенияtype ImmutableUserProfile = Readonly<UserProfile>;3.5. Оператор satisfies (TS 4.9+)
Заголовок раздела «3.5. Оператор satisfies (TS 4.9+)»satisfies позволяет проверить, что выражение соответствует типу, не сужая при этом его исходный литеральный тип. Полезно для обеспечения соответствия объекта интерфейсу, сохраняя при этом более точные типы.
type Color = "red" | "green" | "blue";
const palette = { primary: "red", secondary: "green", accent: "blue", neutral: "gray" // Ошибка, если тип 'Color' не позволяет 'gray'} satisfies Record<string, Color | string>;// `palette` здесь имеет точный тип `{ primary: "red"; ...; neutral: "gray"; }`// но при этом TS проверяет, что все его значения соответствуют `Color | string`.
// Если бы мы использовали `as`, то `palette.primary` стал бы просто `Color`, теряя "red".// const paletteAs: Record<string, Color | string> = { primary: "red", secondary: "green", accent: "blue", neutral: "gray" };// Тип `paletteAs.primary` был бы `Color | string` (т.е. "red" | "green" | "blue" | string), что менее точно.4. Практика и дальнейшее развитие
Заголовок раздела «4. Практика и дальнейшее развитие»Применяйте эти практики в своих проектах:
- Начинайте с
strict: trueв новом проекте. - Всегда явно типизируйте параметры функций и возвращаемые значения.
- Используйте интерфейсы или псевдонимы типов для сложных структур данных.
- Отдавайте предпочтение объединениям и дискриминирующим объединениям, а не перегрузкам функций или
any. - Изучайте и применяйте утилитарные типы.
- Используйте
constиreadonlyвезде, где это уместно, для создания иммутабельных структур.
Следование этим рекомендациям не только уменьшит количество ошибок в вашем коде, но и значительно улучшит его читаемость, поддерживаемость и масштабируемость.