-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
212: Practice Exercise: Building a Type-Safe Reducer
We've spent a fair amount of time talking about the theory of unions and type narrowing. But if you've ever worked with Redux or the useReducer hook in React, you know that this is where the rubber actually meets the road. A reducer is basically just a big state machine, and if you don't type it correctly, you'll end up with a nightmare of any casts or runtime crashes when an action doesn't have the payload you expected.
Let's build a small shopping cart manager together. I want to keep it simple: we can add items, remove items, and clear the whole cart.
Defining the Shape of our Shopping Cart
First, I need a clear definition of what my state looks like. I'm not going to overcomplicate this; just an array of items and a total price. I'll start by defining the CartItem and the CartState.
interface CartItem {
id: string;
name: string;
price: number;
}
interface CartState {
items: CartItem[];
total: number;
}
const initialState: CartState = {
items: [],
total: 0,
};
Wiring up the Reducer Logic
Now, here is where most people start by just creating a generic Action interface with a type: string. I'll do that for a second just to show you why it's a bad idea.
interface Action {
type: string;
payload?: any;
}
function cartReducer(state: CartState, action: Action): CartState {
switch (action.type) {
case 'ADD_ITEM':
// I'm assuming the payload is a CartItem here
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
default:
return state;
}
}
Wait, why is this property undefined?
Look at that ADD_ITEM case. I'm accessing action.payload.price. Because I typed the payload as any, TypeScript isn't complaining yet. But if I accidentally pass a string as the payload, or forget the payload entirely, the app crashes at runtime. That's exactly what we're trying to avoid. Even worse, if I want to add a REMOVE_ITEM action that takes an id: string instead of a whole CartItem, my Action interface becomes a confusing mess of optional properties.
I've made this mistake in plenty of projects—relying on any because "it's just a simple action." It's a trap. Let's fix this using a discriminated union.
Tightening the Types with Discriminated Unions
The trick is to define each action as its own type and then union them together. This way, when we check the type property in our switch statement, TypeScript "narrows" the action to the specific type that matches that string. It knows exactly what the payload is for every single case.
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: string } // only need the ID here
| { type: 'CLEAR_CART' }; // no payload needed
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM':
// TypeScript now knows action.payload is a CartItem
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price,
};
case 'REMOVE_ITEM':
// TypeScript now knows action.payload is a string (the ID)
const filteredItems = state.items.filter(item => item.id !== action.payload);
return {
...state,
items: filteredItems,
total: filteredItems.reduce((sum, item) => sum + item.price, 0),
};
case 'CLEAR_CART':
return initialState;
default:
// This part is crucial for exhaustive checking
return state;
}
}
Now, if I try to access action.payload.price inside the REMOVE_ITEM block, TypeScript will throw an error because it knows the payload is just a string. This is where the power of TypeScript really shines in state management—it forces you to handle the data correctly for every specific action type.
📋 Practical Task
Exercise: Expanding the Cart Reducer with Quantity Logic
In the previous example, we only handled adding and removing items. Your task is to update the CartAction union and the cartReducer to handle updating the quantity of an item already in the cart.
- Modify the
CartIteminterface to include aquantity: numberproperty. - Add a new action type to the
CartActionunion calledUPDATE_QUANTITY. It should carry a payload containing both theid: stringof the item and thenewQuantity: number. - Implement the
UPDATE_QUANTITYcase in the reducer. You'll need to map through the items, find the one that matches the ID, and update its quantity while also recalculating thetotalprice of the cart. - Ensure type safety: Make sure you aren't using
anyanywhere in your new logic.
There are no comments for now.