Главная / Статьи / Три шаблона TypeScript, улучшающих архитектуру React-приложений

Три шаблона TypeScript, улучшающих архитектуру React-приложений

Узнайте, как шаблоны Repository, Observer и Builder используют систему типов TypeScript для создания более чистых и удобных в обслуживании кодовых баз React и Next.js.

763 слов

Написание кода на TypeScript не означает его правильного использования

Многие разработчики просто добавляют аннотации типов к своим переменным и считают, что всё сделано. Но настоящая сила TypeScript кроется в чём-то другом — в структурных паттернах, которые помогают превратить хрупкую базу кода в такую, которая постепенно развивается без проблем.

После работы над производственными приложениями с React и Next.js я выделил несколько паттернов, которые действительно меняют ситуацию к лучшему. Это не абстрактные учебные задания — это решения проблем, с которыми вы действительно столкнётесь.

1. Паттерн репозитория — отделите получение данных от всего остального

Когда ваши компоненты напрямую вызывают fetch(), вы создаёте условия для сложной переработки кода при любых изменениях API.

Паттерн репозитория скрывает доступ к данным за чистым интерфейсом:

// repositories/userRepository.ts
interface UserRepository {
  getById(id: string): Promise<User>;
  getAll(): Promise<User[]>;
}

export class ApiUserRepository implements UserRepository {
  async getById(id: string): Promise<User> {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  }

  async getAll(): Promise<User[]> {
    const res = await fetch('/api/users');
    return res.json();
  }
}

Благодаря этому ваши компоненты зависят от абстракции, а не от конкретной реализации. Это позволяет заменять ApiUserRepository на имитацию в тестах, не трогая код интерфейса.

2. Паттерн наблюдателя — реактивное состояние без избыточности Redux

Redux справляется с задачей, но для не особенно сложного состояния его использование может казаться избыточным, как использование топора для мелкой работы. Легкий вариант реализации паттерна наблюдателя на основе классов и типизированного эмиттера событий предлагает более простую альтернативу:

// utils/eventBus.ts
type EventMap = {
  'user:loggedIn': { userId: string };
  'cart:updated': { itemCount: number };
};

class TypedEventBus {
  private listeners: Partial<{
    [K in keyof EventMap]: ((payload: EventMap[K]) => void)[]
  }> = {};

  on<K extends keyof EventMap>(event: K, cb: (payload: EventMap[K]) => void) {
    (this.listeners[event] ??= []).push(cb);
  }

  emit<K extends keyof EventMap>(event: K, payload: EventMap[K]) {
    this.listeners[event]?.forEach(cb => cb(payload));
  }
}

export const eventBus = new TypedEventBus();

Здесь всё имеет строгую типизацию — нет скрытых типов any. Это исключает неоднозначность относительно того, какой формы должен быть пакет данных, ожидаемый конкретным слушателем.

3. Паттерн строителя — порядок при создании сложных объектов

Если вы когда-либо сталкивались с созданием объектов фильтров, конфигураций запросов к API или схем форм, наполненных вложенными условиями, шаблон Builder сразу же придает ясность:

// builders/queryBuilder.ts
class QueryBuilder {
  private params: Record<string, string> = {};

  withPage(page: number) {
    this.params['page'] = String(page);
    return this;
  }

  withLimit(limit: number) {
    this.params['limit'] = String(limit);
    return this;
  }

  withSearch(term: string) {
    if (term.trim()) this.params['q'] = term;
    return this;
  }

  build(): string {
    return new URLSearchParams(this.params).toString();
  }
}

// Usage
const query = new QueryBuilder()
  .withPage(1)
  .withLimit(20)
  .withSearch('typescript')
  .build();
// → "page=1&limit=20&q=typescript"

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

Основные выводы

  • Шаблон Repository: разделяет слой доступа к данным от логики интерфейса, следуя принципу инверсии зависимостей
  • Шаблон Observer: обеспечивает легкую, реактивную коммуникацию между частями приложения без избыточного кода
  • Шаблон Builder: делает создание сложных объектов одновременно понятным и безопасным
  • Все три шаблона значительно выигрывают от интерфейсов и генериков TypeScript, что делает их заметно чище по сравнению с аналогами на обычном JavaScript

«TypeScript — это не просто добавление типов; он создаёт взаимодействие между всеми слоями вашего приложения». — Дэн Вандеркам, автор книги Effective TypeScript

Что попробовать дальше

Выберите один из описанных в этой статье шаблонов и примените его к реальному модулю в вашей кодовой базе на этой неделе. Работайте с каждым шаблоном по отдельности и помните, что это инструменты, а не догмы.

Самые надёжные кодовые базы определяются не количеством типов, даже в проектах на TypeScript. Они определяются целенаправленностью — каждый шаблон используется потому, что заслужил своё место.

Связанная литература

  • Когда ИИ пишет ваш React-приложение, но игнорирует принципы чистого кода — Узнайте о семи привычках написания чистого кода — DRY, единая ответственность, защитные клозы и другие — которые часто нарушаются в коде React, сгенерированном ИИ, и как их исправить.
  • Понимание React Lanes: как битмаски кодируют приоритет обновлений — Узнайте, как React использует битмаски для кодирования нескольких приоритетов обновлений в одно целое число, и почему для планирования используются битовые операции вместо простых логических флагов.