Home / Articles / Three TypeScript Patterns That Improve React App Architecture

This article is published in English.

Three TypeScript Patterns That Improve React App Architecture

Learn how the Repository, Observer, and Builder patterns use TypeScript's type system to create cleaner, more maintainable React and Next.js codebases.

763 words

Writing TypeScript doesn't mean using it well

Plenty of developers slap type annotations on their variables and call it a day. But the real strength of TypeScript lies elsewhere — in the structural patterns that separate a fragile codebase from one that grows gracefully over time.

Having spent time building production apps with React and Next.js, a handful of patterns stand out as genuine game-changers. These aren't abstract textbook exercises — they're solutions to problems you'll actually run into.

1. Repository Pattern — Separate Your Data Fetching from Everything Else

When your components call fetch() directly, you're setting yourself up for a painful rewrite the moment an API changes shape.

The Repository Pattern wraps data access behind a clean interface:

// 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();
  }
}

With this in place, your components depend on an abstraction rather than a concrete implementation. That means you can swap ApiUserRepository for a mock version in your tests without touching any UI code.

2. Observer Pattern — Reactive State Without the Redux Overhead

Redux gets the job done, but for state that isn't especially complex, it can feel like using a sledgehammer for a small task. A lightweight, class-based Observer implementation built on a typed event emitter offers a simpler 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();

Everything here is strongly typed — no any types hiding in the shadows. That means there's no ambiguity about what shape of payload a given listener should expect.

3. Builder Pattern — Bring Order to Complex Object Creation

If you've ever wrestled with constructing filter objects, API request configurations, or form schemas full of nested conditionals, the Builder Pattern brings immediate clarity:

// 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"

The resulting code reads naturally, chains together cleanly, and makes it structurally impossible to call the steps in the wrong sequence.

Key Takeaways

  • Repository Pattern: separates your data-access layer from your UI logic, following the dependency inversion principle
  • Observer Pattern: enables lightweight, reactive communication between parts of your app without heavy boilerplate
  • Builder Pattern: makes constructing complex objects both readable and safe
  • All three patterns benefit substantially from TypeScript's interfaces and generics, making them noticeably cleaner than their plain JavaScript counterparts

"TypeScript isn't just about adding types — it creates a dialogue between every layer of your application." — Dan Vanderkam, author of Effective TypeScript

What to Try Next

Pick one pattern from this article and apply it to a real module in your codebase this week. Work through one vertical slice at a time, and remember that these patterns are tools, not dogma.

The strongest codebases aren't defined by how many types they contain, even in a TypeScript project. They're defined by intentionality — where every pattern is there because it's earned its place.