Это руководство охватывает ключевые лучшие практики TypeScript.
Цель - помочь вам писать чистый, поддерживаемый и типобезопасный код. Следование этим рекомендациям повысит качество кода и удобство работы разработчиков.
Конфигурация проекта
Включите строгий режим
Всегда активируйте опцию strict в tsconfig.json - это гарантирует максимальную типобезопасность:
// tsconfig.json
{
"compilerOptions": {
/* Включить все опции строгой проверки типов */
"strict": true,
/* Дополнительные рекомендуемые настройки */
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Дополнительные строгие проверки
Можно включить эти опции для повышения качества кода:
{
"compilerOptions": {
/* Дополнительные строгие проверки */
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Лучшие практики системы типов
Используйте вывод типов там, где это возможно
Позвольте TypeScript самому определять типы, если они очевидны из присваивания:
// Плохо: избыточная аннотация типа
const name: string = 'John';
// Хорошо: пусть TypeScript выведет тип
const name = 'John';
// Плохо: избыточный тип возвращаемого значения
function add(a: number, b: number): number {
return a + b;
}
// Хорошо: пусть TypeScript выведет возвращаемый тип
function add(a: number, b: number) {
return a + b;
}
Точные аннотации типов
Явно указывайте типы для публичных API и параметров функций:
// Плохо: нет информации о типах
function processUser(user) {
return user.name.toUpperCase();
}
// Хорошо: явные типы параметров и возвращаемого значения
interface User {
id: number;
name: string;
email?: string; // Необязательное свойство
}
function processUser(user: User): string {
return user.name.toUpperCase();
}
Интерфейсы vs. Псевдонимы типов
Знайте, когда использовать interface, а когда type:
// Используйте interface для форм объектов, которые можно расширять/реализовывать
interface User {
id: number;
name: string;
}
// Расширение интерфейса
interface AdminUser extends User {
permissions: string[];
}
// Используйте type для объединений, кортежей или отображаемых типов
type UserRole = 'admin' | 'editor' | 'viewer';
// Объединения типов
type UserId = number | string;
// Отображаемые типы
type ReadonlyUser = Readonly<User>;
// Кортежи
type Point = [number, number];
Избегайте типа any
Используйте более конкретные типы вместо any:
// Плохо: теряется типобезопасность
function logValue(value: any) {
console.log(value.toUpperCase()); // Ошибка только во время выполнения
}
// Лучше: используйте обобщённый параметр типа
function logValue<T>(value: T) {
console.log(String(value)); // Безопаснее, но всё равно не идеально
}
// Оптимально: чётко укажите ожидаемые типы
function logString(value: string) {
console.log(value.toUpperCase()); // Типобезопасно
}
// Когда нужно принять любое значение, но сохранить типобезопасность
function logUnknown(value: unknown) {
if (typeof value === 'string') {
console.log(value.toUpperCase());
} else {
console.log(String(value));
}
}
Организация кода
Модульная организация
Разделяйте код на логические модули с чёткими обязанностями:
// user/user.model.ts
export interface User {
id: string;
name: string;
email: string;
}
// user/user.service.ts
import { User } from './user.model';
export class UserService {
private users: User[] = [];
addUser(user: User) {
this.users.push(user);
}
getUser(id: string): User | undefined {
return this.users.find(user => user.id === id);
}
}
// user/index.ts (баррель‑файл)
export * from './user.model';
export * from './user.service';
Соглашения об именовании файлов
Соблюдайте единообразие в именовании:
// Хорошо
user.service.ts // Классы‑сервисы
user.model.ts // Определения типов
user.controller.ts // Контроллеры
user.component.ts // Компоненты
user.utils.ts // Вспомогательные функции
user.test.ts // Тестовые файлы
// Плохо
UserService.ts // Избегайте PascalCase для имён файлов
user_service.ts // Избегайте snake_case
userService.ts // Избегайте camelCase для имён файлов
Ключевые рекомендации
- Документируйте свои типы и интерфейсы.
- Предпочитайте композицию наследованию для типов.
- Поддерживайте
tsconfig.jsonв строгом режиме и актуальном состоянии. - Проводите рефакторинг кода, чтобы использовать более конкретные типы по мере развития кодовой базы.
Функции и методы
Параметры и возвращаемые типы функций
Пишите понятные и типобезопасные функции с чёткими типами параметров и возвращаемых значений:
// Плохо: нет информации о типах
function process(user, notify) {
notify(user.name);
}
// Хорошо: явные типы параметров и возвращаемого значения
function processUser(
user: User,
notify: (message: string) => void
): void {
notify(`Processing user: ${user.name}`);
}
// Используйте параметры по умолчанию вместо условных проверок
function createUser(
name: string,
role: UserRole = 'viewer',
isActive: boolean = true
): User {
return { name, role, isActive };
}
// Используйте остаточные параметры для переменного числа аргументов
function sum(...numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}
Избегайте чрезмерной сложности функций
Следите за сложностью и обязанностями функций:
// Плохо: слишком много обязанностей
function processUserData(userData: any) {
// Валидация
if (!userData || !userData.name) throw new Error('Invalid user data');
// Преобразование данных
const processedData = {
...userData,
name: userData.name.trim(),
createdAt: new Date()
};
// Побочный эффект
saveToDatabase(processedData);
// Уведомление
sendNotification(processedData.email, 'Profile updated');
return processedData;
}
// Лучше: разделите на небольшие, сфокусированные функции
function validateUserData(data: unknown): UserData {
if (!data || typeof data !== 'object') {
throw new Error('Invalid user data');
}
return data as UserData;
}
function processUserData(userData: UserData): ProcessedUserData {
return {
...userData,
name: userData.name.trim(),
createdAt: new Date()
};
}
Паттерны async/await
Правильное использование async/await
Эффективно обрабатывайте асинхронные операции - обязательно предусматривайте обработку ошибок:
// Плохо: нет обработки ошибок
async function fetchData() {
const response = await fetch('/api/data');
return response.json();
}
// Хорошо: должная обработка ошибок
async function fetchData<T>(url: string): Promise<T> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP‑ошибка! Статус: ${response.status}`);
}
return await response.json() as T;
} catch (error) {
console.error('Не удалось загрузить данные:', error);
throw error; // Повторно выбрасываем, чтобы вызывающий код мог обработать
}
}
// Лучше: используйте Promise.all для параллельных операций
async function fetchMultipleData<T>(urls: string[]): Promise<T[]> {
try {
const promises = urls.map(url => fetchData<T>(url));
return await Promise.all(promises);
} catch (error) {
console.error('Одна или несколько операций завершились ошибкой:', error);
throw error;
}
}
// Пример использования
interface User {
id: string;
name: string;
email: string;
}
// Загрузка данных пользователя с корректной типизацией
async function getUserData(userId: string): Promise<User> {
return fetchData<User>(`/api/users/${userId}`);
}
Избегайте вложенных конструкций async/await
Упрощайте код с async/await - избегайте "ада обратных вызовов":
// Плохо: вложенные async/await ("ад обратных вызовов")
async function processUser(userId: string) {
const user = await getUser(userId);
if (user) {
const orders = await getOrders(user.id);
if (orders.length > 0) {
const latestOrder = orders[0];
const items = await getOrderItems(latestOrder.id);
return { user, latestOrder, items };
}
}
return null;
}
// Лучше: упростите цепочку async/await
async function processUser(userId: string) {
const user = await getUser(userId);
if (!user) return null;
const orders = await getOrders(user.id);
if (orders.length === 0) return { user, latestOrder: null, items: [] };
const latestOrder = orders[0];
const items = await getOrderItems(latestOrder.id);
return { user, latestOrder, items };
}
// Оптимально: используйте Promise.all для независимых асинхронных операций
async function processUser(userId: string) {
const [user, orders] = await Promise.all([
getUser(userId),
getOrders(userId)
]);
if (!user) return null;
if (orders.length === 0) return { user, latestOrder: null, items: [] };
const latestOrder = orders[0];
const items = await getOrderItems(latestOrder.id);
return { user, latestOrder, items };
}
Тестирование и качество кода
Написание пригодного к тестированию кода
Проектируйте код с учётом тестируемости - используйте внедрение зависимостей и чистые функции:
// Плохо: сложно тестировать из‑за прямых зависимостей
class PaymentProcessor {
async processPayment(amount: number) {
const paymentGateway = new PaymentGateway();
return paymentGateway.charge(amount);
}
}
// Лучше: используйте внедрение зависимостей
interface PaymentGateway {
charge(amount: number): Promise<boolean>;
}
class PaymentProcessor {
constructor(private paymentGateway: PaymentGateway) {}
async processPayment(amount: number): Promise<boolean> {
if (amount <= 0) {
throw new Error('Сумма должна быть больше нуля');
}
return this.paymentGateway.charge(amount);
}
}
// Пример теста с Jest
describe('PaymentProcessor', () => {
let processor: PaymentProcessor;
let mockGateway: jest.Mocked<PaymentGateway>;
beforeEach(() => {
mockGateway = {
charge: jest.fn()
};
processor = new PaymentProcessor(mockGateway);
});
it('должен обработать корректный платёж', async () => {
mockGateway.charge.mockResolvedValue(true);
const result = await processor.processPayment(100);
expect(result).toBe(true);
expect(mockGateway.charge).toHaveBeenCalledWith(100);
});
it('должен выбросить ошибку для некорректной суммы', async () => {
await expect(processor.processPayment(-50))
.rejects
.toThrow('Сумма должна быть больше нуля');
});
});
Тестирование типов
Проверяйте типы, чтобы убедиться, что они работают как ожидается - используйте утверждения типов и вспомогательные утилиты:
// Использование @ts-expect-error для проверки ошибок типов
// @ts-expect-error — не должно позволять отрицательные значения
const invalidUser: User = { id: -1, name: 'Test' };
// Использование утверждений типов в тестах
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Не строка');
}
}
// Использование вспомогательных типов для тестирования
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
// Использование tsd для тестирования типов (установка: npm install --save-dev tsd)
/*
import { expectType } from 'tsd';
const user = { id: 1, name: 'John' };
expectType<{ id: number; name: string }>(user);
expectType<string>(user.name);
*/
Производительность
Импорты и экспорты только для типов
Используйте импорты и экспорты только для типов - это уменьшит размер сборки и улучшит исключение неиспользуемого кода:
// Плохо: импортирует и тип, и значение
import { User, fetchUser } from './api';
// Хорошо: разделите импорты типов и значений
import type { User } from './api';
import { fetchUser } from './api';
// Ещё лучше: по возможности используйте импорты только для типов
import type { User, UserSettings } from './types';
// Экспорт только для типов
export type { User };
// Экспорт для выполнения (runtime)
export { fetchUser };
// В tsconfig.json включите "isolatedModules": true
// чтобы корректно обрабатывать импорты только для типов
Избегайте излишней сложности типов
Помните о сложных типах - они могут замедлить компиляцию:
// Плохо: глубоко вложенные отображаемые типы могут работать медленно
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Лучше: по возможности используйте встроенные вспомогательные типы
type User = {
id: string;
profile: {
name: string;
email: string;
};
preferences?: {
notifications: boolean;
};
};
// Вместо DeepPartial<User> используйте Partial с утверждениями типов
const updateUser = (updates: Partial<User>) => {
// Реализация
};
// Для сложных типов рассмотрите использование интерфейсов
interface UserProfile {
name: string;
email: string;
}
interface UserPreferences {
notifications: boolean;
}
interface User {
id: string;
profile: UserProfile;
preferences?: UserPreferences;
}
Используйте утверждения const для литеральных типов
Улучшайте вывод типов и производительность с помощью утверждений const:
// Без утверждения const (более широкий тип)
const colors = ['red', 'green', 'blue'];
// Тип: string[]
// С утверждением const (более узкий, точный тип)
const colors = ['red', 'green', 'blue'] as const;
// Тип: readonly ["red", "green", "blue"]
// Извлечение объединения типов из массива const
type Color = typeof colors[number]; // "red" | "green" | "blue"
// Объекты с утверждениями const
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
features: ['auth', 'notifications'],
} as const;
// Полученный тип:
// {
// readonly apiUrl: "https://api.example.com";
// readonly timeout: 5000;
// readonly features: readonly ["auth", "notifications"];
// }
Распространённые ошибки, которых следует избегать
Чрезмерное использование типа any
Избегайте использования any - он отключает проверку типов в TypeScript:
// Плохо: теряется вся типобезопасность
function process(data: any) {
return data.map(item => item.name);
}
// Лучше: используйте обобщения (дженерики) для типобезопасности
function process<T extends { name: string }>(items: T[]) {
return items.map(item => item.name);
}
// Оптимально: по возможности используйте конкретные типы
interface User {
name: string;
age: number;
}
function processUsers(users: User[]) {
return users.map(user => user.name);
}
Неиспользование строгого режима
Всегда включайте строгий режим в tsconfig.json:
// tsconfig.json
{
"compilerOptions": {
"strict": true,
/* Дополнительные флаги строгой проверки */
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
Игнорирование вывода типов
Позвольте TypeScript выводить типы автоматически, когда это возможно:
// Избыточная аннотация типа
const name: string = 'John';
// Пусть TypeScript выведет тип
const name = 'John'; // TypeScript понимает, что это строка
// Избыточный тип возвращаемого значения
function add(a: number, b: number): number {
return a + b;
}
// Пусть TypeScript выведет возвращаемый тип
function add(a: number, b: number) {
return a + b; // TypeScript выводит тип number
}
Неиспользование защитников типов
Используйте защитники типов, чтобы безопасно сужать типы:
// Без защитной проверки типа
function process(input: string | number) {
return input.toUpperCase(); // Ошибка: toUpperCase не существует у number
}
// С защитной проверкой типа
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function process(input: string | number) {
if (isString(input)) {
return input.toUpperCase(); // TypeScript знает: input — это строка
} else {
return input.toFixed(2); // TypeScript знает: input — это число
}
}
// Встроенные защитные проверки типов
if (typeof value === 'string') { /* value — строка */ }
if (value instanceof Date) { /* value — объект Date */ }
if ('id' in user) { /* у объекта user есть свойство id */ }
Необработка значений null и undefined
Всегда обрабатывайте потенциальные значения null или undefined:
// Плохо: возможна ошибка во время выполнения
function getLength(str: string | null) {
return str.length; // Ошибка: объект может быть null
}
// Хорошо: проверка на null
function getLength(str: string | null) {
if (str === null) return 0;
return str.length;
}
// Лучше: используйте опциональную цепочку и оператор нулевого слияния
function getLength(str: string | null) {
return str?.length ?? 0;
}
// Для массивов
const names: string[] | undefined = [];
const count = names?.length ?? 0; // Безопасная обработка undefined
// Для свойств объектов
interface User {
profile?: {
name?: string;
};
}
const user: User = {};
const name = user.profile?.name ?? 'Аноним';