Три шаблони 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.