TypeScript Лучшие практики

Это руководство охватывает ключевые лучшие практики 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 ?? 'Аноним';