Careeroza
Real World Example
Typed Redux-style Actions
State management libraries (Redux Toolkit, Zustand) and API clients (tRPC) rely on infer and conditional types so that every action creator and handler is typed from one definition. Here a map of action creators produces a fully typed dispatch.
Example
const actions = {
addItem: (sku: string, qty: number) => ({ sku, qty }),
removeItem: (sku: string) => ({ sku }),
clear: () => ({}),
};
type Actions = typeof actions;
type ActionName = keyof Actions;
type Action = { [K in ActionName]: { type: K; payload: ReturnType<Actions[K]> } }[ActionName];
type Cart = Record<string, number>;
function reducer(state: Cart, action: Action): Cart {
switch (action.type) {
case "addItem": return { ...state, [action.payload.sku]: (state[action.payload.sku] ?? 0) + action.payload.qty };
case "removeItem": { const { [action.payload.sku]: _, ...rest } = state; return rest; }
case "clear": return {};
}
}
let state: Cart = {};
function dispatch<K extends ActionName>(type: K, ...args: Parameters<Actions[K]>): void {
const creator = actions[type] as (...a: Parameters<Actions[K]>) => ReturnType<Actions[K]>;
state = reducer(state, { type, payload: creator(...args) } as Action);
console.log(type.padEnd(10), JSON.stringify(state));
}
dispatch("addItem", "SKU-1", 2);
dispatch("addItem", "SKU-2", 1);
dispatch("addItem", "SKU-1", 1);
dispatch("removeItem", "SKU-2");
dispatch("clear");Output
addItem {"SKU-1":2}
addItem {"SKU-1":2,"SKU-2":1}
addItem {"SKU-1":3,"SKU-2":1}
removeItem {"SKU-1":3}
clear {}Note: Writing dispatch("addItem", "SKU-1") (missing qty) or dispatch("remove", ...) is a compile error: argument types come from Parameters<Actions[K]>.