Vanilla TypeScript Core State Management Patterns

The foundational approach is a typed observable store — a generic Store<T> class that holds state, exposes getState(), and notifies subscribers via setState(). TypeScript generics enforce that only valid keys/shapes can be set. Three main patterns build on this:

  • Observable Store — a central class with subscribe/setState, good for simple-to-medium apps

  • Reducer — Redux-style discriminated union actions processed by a pure reducer function; TypeScript’s exhaustive checking ensures no action is missed

  • Proxy-based reactivity — intercepts property assignments to auto-trigger re-renders, mimicking Vue/MobX without a library

  • Path-based (DeepState) - Best for bested state trees

  • Persistent State - Best for cross-session state

    Best practices: always define a state interface, use Partial<T> for updates, prefer immutable spreads, and normalise collections as { byId, allIds }.

Difference between observer, singleton channel and BroadcastChannel

  1. Observer — one object talking to its own watchers

  2. Singleton Channel — any part of your app talking to any other part, within the same tab

  3. BroadcastChannel — any tab talking to any other tab in the same browser

In practice, you’d often use all three together: a BroadcastChannel receives a cross-tab message → publishes onto a singleton Channel<T> → individual components subscribed via the Observer pattern react to the update.

TypeScript-Specific Best Practices

  • Always define a state interface — never use any for your state shape ​

  • Use Partial<T> for updates — so you only pass the fields you’re changing

  • Prefer immutable updates — spread operators ({ ...state, ...updates }) instead of mutating directly ​

  • Normalize entity collections — store arrays as { byId: Record<id, T>, allIds: id[] } for O(1) lookups ​

  • Use localStorage with version migrations — persist state but include a _version field so you can safely migrate old data

8 items under this folder.