Три шаблона TypeScript, улучшающих архитектуру React-приложений
Узнайте, как шаблоны Repository, Observer и Builder используют систему типов TypeScript для создания более чистых и удобных в обслуживании кодовых баз React и Next.js.
Написание кода на 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. Они определяются целенаправленностью — каждый шаблон используется потому, что заслужил своё место.
Связанная литература
- Компилятор TypeScript на Go и нативная реализация: руководство по миграции — Узнайте, как компилятор TypeScript на основе Go и нативная реализация в Node.js повлияют на кодовые базы React и Next.js, и что необходимо исправить в вашем tsconfig прямо сейчас.
- Автоматическое генерирование типобезопасного клиента API для Next.js из NestJS Swagger — Узнайте, как устранить дублирование типов API с помощью NestJS Swagger и Orval для автоматического генерирования типобезопасных хуков React Query для Next.js.