Drei TypeScript-Muster, die die Architektur von React-Apps verbessern
Erfahren Sie, wie die Repository-, Observer- und Builder-Muster das Typsystem von TypeScript nutzen, um sauberere und wartbarere React- sowie Next.js-Codebasen zu erstellen.
TypScript schreiben bedeutet nicht, es gut zu nutzen
Viele Entwickler fügen ihren Variablen einfach Typangaben hinzu und halten das für ausreichend. Doch die wahre Stärke von TypeScript liegt woanders – in den strukturellen Mustern, die eine brüchige Codebasis von einer solchen unterscheiden, die im Laufe der Zeit gut wächst.
Nachdem ich Zeit damit verbracht habe, Produktionsanwendungen mit React und Next.js zu entwickeln, gibt es einige Muster, die sich als echte Game-Changers herausgestellt haben. Es handelt sich dabei nicht um abstrakte Übungen aus Lehrbüchern – sondern um Lösungen für Probleme, auf die man tatsächlich stoßen wird.
1. Repository-Muster – Trennen Sie das Datenabrufen von allem anderen
Wenn Ihre Komponenten direkt fetch() aufrufen, bereiten Sie sich darauf vor, im Falle einer Änderung der API einen mühsamen Neuschreibprozess durchführen zu müssen.
Das Repository-Muster verbirgt den Datenzugriff hinter einer sauberen Schnittstelle:
// 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();
}
}
Dadurch sind Ihre Komponenten von einer Abstraktion und nicht von einer konkreten Implementierung abhängig. Das bedeutet, Sie können in Ihren Tests ApiUserRepository durch eine Mock-Version ersetzen, ohne irgendeinen UI-Code anzufassen.
2. Observer-Muster – reaktiver Zustand ohne den Redux-Overhead
Redux erledigt die Aufgabe, aber für Zustände, die nicht besonders komplex sind, kann es sich anfühlen, als würde man für eine kleine Aufgabe einen Vorschlaghammer verwenden. Eine leichte, klassenbasierte Observer-Implementierung, die auf einem typisierten Event-Emitter basiert, bietet eine einfachere Alternative:
// 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();
Alles hier ist stark typisiert – es gibt keine any-Typen, die im Verborgenen lauern. Das bedeutet, es besteht keine Unklarheit darüber, welche Form des Payloads ein bestimmter Zuhörer erwarten sollte.
3. Builder-Muster – Ordnung in der Erstellung komplexer Objekte schaffen
Falls Sie schon einmal mit der Erstellung von Filterobjekten, Konfigurationen für API-Anfragen oder Formschemata voller verschachtelter Bedingungen zu kämpfen hatten, bringt das Builder-Muster sofortige Klarheit:
// 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"
Der resultierende Code ist leicht verständlich, lässt sich sauber miteinander verknüpfen, und es ist strukturell unmöglich, die Schritte in der falschen Reihenfolge aufzurufen.
Haupterkenntnisse
- Repository-Muster: Trennt die Datenzugriffsschicht von der UI-Logik und folgt dem Prinzip der Abhängigkeitsumkehr
- Observer-Muster: Ermöglicht eine leichte, reaktive Kommunikation zwischen den Teilen der Anwendung ohne umfangreichen Boilerplate
- Builder-Muster: Macht die Erstellung komplexer Objekte sowohl lesbar als auch sicher
- Sämtliche drei Muster profitieren erheblich von TypeScripts Schnittstellen und Generikas, wodurch sie deutlich sauberer sind als ihre Entsprechungen in reinem JavaScript
„TypeScript geht nicht nur darum, Typen hinzuzufügen – es schafft eine Verbindung zwischen allen Schichten Ihrer Anwendung.“ – Dan Vanderkam, Autor von Effective TypeScript
Was Sie als Nächstes ausprobieren sollten
Wählen Sie ein Muster aus diesem Artikel und wenden Sie es diese Woche auf ein echtes Modul in Ihrer Codebasis an. Arbeiten Sie jeweils Schritt für Schritt voran und denken Sie daran, dass es sich bei diesen Mustern um Werkzeuge und nicht um Dogmen handelt.
Die stärksten Codebasen werden nicht dadurch definiert, wie viele Typen sie enthalten – selbst in einem TypeScript-Projekt nicht. Sie werden durch Absichtlichkeit definiert: Jedes Muster ist dort, weil es seinen Platz verdient hat.
Zusätzliche Lektüre
- TypeScript’s Go Compiler und native Ausführung: Ein Migrationsleitfaden — Erfahren Sie, wie TypeScripts auf Go basierender Compiler sowie die native Ausführung in Node.js die Codebasen von React und Next.js beeinflussen und was Sie jetzt in Ihrer tsconfig korrigieren sollten.
- Automatisches Erstellen eines typsicheren Next.js API Clients aus NestJS Swagger — Lernen Sie, wie Sie durch die Verwendung von NestJS Swagger und Orval doppelte API-Typen beseitigen und typsichere React Query Hooks für Next.js automatisch erstellen können.