Type-Safe State Management
📖 Concept
State management libraries like Redux Toolkit (RTK), Zustand, and Jotai have first-class TypeScript support. Typing state correctly prevents entire categories of bugs — wrong action payloads, accessing non-existent state, and stale selectors.
Redux Toolkit (RTK):
createSliceinfers action types and payload types from reducers- Typed
useSelectoranduseDispatchhooks via a customRootStatetype createAsyncThunktypes async action lifecycle (pending/fulfilled/rejected)
Zustand: Simpler API, excellent TypeScript inference. Define the store type and Zustand infers everything.
Key patterns:
- Typed root state — Derive from store:
type RootState = ReturnType<typeof store.getState> - Typed dispatch —
type AppDispatch = typeof store.dispatch - Typed selectors —
(state: RootState) => state.user.name - Discriminated union actions — Each action has a unique
typewith typedpayload
🏠 Real-world analogy: Typed state management is like a typed spreadsheet. Each column has a defined type (text, number, date), and the spreadsheet rejects invalid entries. Without types, it's like a free-form notebook — anything goes, and errors are discovered when you try to use the data.
💻 Code Example
1// ========== REDUX TOOLKIT ==========2// import { configureStore, createSlice, PayloadAction } from "@reduxjs/toolkit";34// Define state shape5interface TodoState {6 items: Todo[];7 filter: "all" | "active" | "completed";8 loading: boolean;9}1011interface Todo {12 id: string;13 text: string;14 completed: boolean;15 createdAt: number;16}1718const initialState: TodoState = {19 items: [],20 filter: "all",21 loading: false,22};2324// createSlice — actions are auto-typed from reducers25// const todoSlice = createSlice({26// name: "todos",27// initialState,28// reducers: {29// addTodo(state, action: PayloadAction<{ text: string }>) {30// state.items.push({31// id: crypto.randomUUID(),32// text: action.payload.text,33// completed: false,34// createdAt: Date.now(),35// });36// },37// toggleTodo(state, action: PayloadAction<{ id: string }>) {38// const todo = state.items.find((t) => t.id === action.payload.id);39// if (todo) todo.completed = !todo.completed;40// },41// removeTodo(state, action: PayloadAction<{ id: string }>) {42// state.items = state.items.filter((t) => t.id !== action.payload.id);43// },44// setFilter(state, action: PayloadAction<TodoState["filter"]>) {45// state.filter = action.payload;46// },47// },48// });4950// Typed hooks (create once, use everywhere)51// type RootState = ReturnType<typeof store.getState>;52// type AppDispatch = typeof store.dispatch;53// const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;54// const useAppDispatch = () => useDispatch<AppDispatch>();5556// Usage in component:57// const todos = useAppSelector((state) => state.todos.items);58// const dispatch = useAppDispatch();59// dispatch(todoSlice.actions.addTodo({ text: "Learn TS" })); // ✅ Type-safe6061// ========== ZUSTAND ==========62// import { create } from "zustand";6364interface AuthStore {65 user: { id: string; name: string; email: string } | null;66 token: string | null;67 isAuthenticated: boolean;68 login: (email: string, password: string) => Promise<void>;69 logout: () => void;70 updateProfile: (updates: Partial<{ name: string; email: string }>) => void;71}7273// const useAuthStore = create<AuthStore>((set, get) => ({74// user: null,75// token: null,76// isAuthenticated: false,77//78// login: async (email, password) => {79// const res = await fetch("/api/login", {80// method: "POST",81// body: JSON.stringify({ email, password }),82// });83// const { user, token } = await res.json();84// set({ user, token, isAuthenticated: true });85// },86//87// logout: () => {88// set({ user: null, token: null, isAuthenticated: false });89// },90//91// updateProfile: (updates) => {92// const current = get().user;93// if (current) {94// set({ user: { ...current, ...updates } });95// }96// },97// }));9899// Usage: const { user, login } = useAuthStore();100// Fully typed — login expects (string, string), user is typed or null101102// Typed selectors pattern103// const userName = useAuthStore((state) => state.user?.name);104// Type: string | undefined105106console.log("State management with full TypeScript support");
🏋️ Practice Exercise
Mini Exercise:
- Create a Redux Toolkit slice with typed actions and selectors for a shopping cart
- Build a Zustand store with typed state and actions for a theme manager
- Type an async thunk that fetches paginated data with typed request/response
- Create typed selector functions that derive computed state
- Use
PayloadAction<T>to ensure action payloads match expected types
⚠️ Common Mistakes
Not creating typed hooks (
useAppSelector,useAppDispatch) — using untypeduseSelectorloses all type safetyTyping Redux state as
any— always defineRootState = ReturnType<typeof store.getState>for full inferenceUsing
useSelector(state => state.user)without RootState type — the selector parameter needs the typed root stateNot typing async thunk error cases —
createAsyncThunkhaspending,fulfilled, ANDrejectedstates; type all threeIn Zustand, not providing the generic type —
create<StoreType>()is essential for type inference to work correctly
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Type-Safe State Management