Перейти к содержимому

63. Best practices

TypeScript: Best Practices для создания надежного и масштабируемого кода

Заголовок раздела «TypeScript: Best Practices для создания надежного и масштабируемого кода»

Иллюстрация к уроку

TypeScript, в основе своей, призван помочь разработчикам создавать более надежный и предсказуемый код. Однако, чтобы полностью раскрыть его потенциал, важно следовать определенным “лучшим практикам”. Этот урок погрузит вас в фундаментальные и продвинутые техники, которые сделают ваш TypeScript-код более безопасным, читаемым и легко поддерживаемым.

В чистом JavaScript легко столкнуться с ошибками во время выполнения из-за неверных предположений о типах данных. Отсутствие статической проверки делает рефакторинг рискованным, а отладку — трудоемкой.

Рассмотрим простой пример обработки пользовательских данных:

userProcessor.js
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: " Alice ", email: "[email protected]" });
processUserData({ name: "Bob" }); // Ошибка: email is undefined при вызове split()
processUserData("Charlie"); // Ошибка: data.name is undefined

Этот код изобилует ручными проверками, которые легко пропустить, и он все еще подвержен ошибкам.

2. Решение: Фундаментальная типобезопасность с TypeScript

Заголовок раздела «2. Решение: Фундаментальная типобезопасность с TypeScript»

TypeScript позволяет нам формализовать ожидания от данных, перемещая многие проверки в этап компиляции.

userProcessor.ts
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: " Alice ", email: "[email protected]" }); // OK
// processUserData({ name: "Bob" }); // Ошибка компиляции: Property 'email' is missing
// processUserData("Charlie"); // Ошибка компиляции: Argument of type 'string' is not assignable

Включение strict: true в вашем tsconfig.json активирует набор важных проверок, таких как noImplicitAny, strictNullChecks, strictFunctionTypes и другие. Это наиболее важная практика для надежного кода.

tsconfig.json
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"strict": true, // Это включает множество полезных проверок
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

Для данных, которые не должны изменяться, используйте 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; // OK

3.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"'

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; }
const userUpdate: PartialUserProfile = { email: "[email protected]" }; // OK
// Pick<T, K>: выбирает набор свойств K из T
type UserBasicInfo = Pick<UserProfile, "name" | "email">;
// { name: string; email: string; }
// Omit<T, K>: исключает набор свойств K из T
type UserSensitiveInfo = Omit<UserProfile, "name" | "email">;
// { id: string; isActive: boolean; }
// Readonly<T>: делает все свойства T только для чтения
type ImmutableUserProfile = Readonly<UserProfile>;

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), что менее точно.

Применяйте эти практики в своих проектах:

  • Начинайте с strict: true в новом проекте.
  • Всегда явно типизируйте параметры функций и возвращаемые значения.
  • Используйте интерфейсы или псевдонимы типов для сложных структур данных.
  • Отдавайте предпочтение объединениям и дискриминирующим объединениям, а не перегрузкам функций или any.
  • Изучайте и применяйте утилитарные типы.
  • Используйте const и readonly везде, где это уместно, для создания иммутабельных структур.

Следование этим рекомендациям не только уменьшит количество ошибок в вашем коде, но и значительно улучшит его читаемость, поддерживаемость и масштабируемость.