-
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
111: Practice Exercise: Building a Type-Safe Redux-Like Store
How do I prevent the state from becoming "any" inside the store?
The biggest mistake I see when people first try to build a store is declaring the state as a generic object or, even worse, any. If you do that, you lose all the benefits of TypeScript the moment you try to access a property in your UI. You want to use a Generic type for the store itself so it can adapt to whatever state shape you're using for a specific project.
type Store = {
getState: () => S;
dispatch: (action: any) => void; // We'll fix this 'any' in a second
};
// Now, when I create a store for my Shopping Cart,
// the state is locked to CartState.
interface CartState {
items: { id: string; name: string; price: number }[];
total: number;
}
function createStore<S>(reducer: (state: S, action: any) => S, initialState: S): Store<S> {
let state = initialState;
return {
getState: () => state,
dispatch: (action) => {
state = reducer(state, action);
}
};
}
How do I make sure dispatch only accepts valid actions?
In the code above, dispatch takes any. That's a disaster waiting to happen. You could dispatch { type: 'EXPLODE_COMPUTER' } and TypeScript wouldn't blink. To fix this, we use a Discriminated Union. This is the "secret sauce" of Redux-like patterns in TypeScript. By defining a specific set of action types, we force the developer to only send actions the reducer actually knows how to handle.
type CartAction =
| { type: 'ADD_ITEM'; payload: { id: string; name: string; price: number } }
| { type: 'REMOVE_ITEM'; payload: { id: string } }
| { type: 'CLEAR_CART' };
// Now we update our Store type to use a generic Action type too
type Store<S, A> = {
getState: () => S;
dispatch: (action: A) => void;
};
function createStore<S, A>(reducer: (state: S, action: A) => S, initialState: S): Store<S, A> {
let state = initialState;
return {
getState: () => state,
dispatch: (action) => {
state = reducer(state, action);
}
};
}
What's the cleanest way to write the reducer without losing type safety?
This is where the magic of the Discriminated Union comes in. When you use a switch statement on the type property, TypeScript automatically "narrows" the type of the action. Inside the 'ADD_ITEM' block, TypeScript knows for a fact that action.payload contains the item details. If you try to access action.payload inside the 'CLEAR_CART' block, the compiler will yell at you because that action doesn't have a payload. I personally find this much safer than using if statements.
const cartReducer = (state: CartState, action: CartAction): CartState => {
switch (action.type) {
case 'ADD_ITEM':
// TypeScript knows action.payload has id, name, and price here
return {
...state,
items: [...state.items, action.payload],
total: state.total + action.payload.price
};
case 'REMOVE_ITEM':
// TypeScript knows action.payload only has an id here
const filteredItems = state.items.filter(item => item.id !== action.payload.id);
return {
...state,
items: filteredItems,
total: filteredItems.reduce((acc, item) => acc + item.price, 0)
};
case 'CLEAR_CART':
return { items: [], total: 0 };
default:
return state;
}
};
const cartStore = createStore(cartReducer, { items: [], total: 0 });
📋 Practical Task
Exercise: Implementing a Cart Discount Feature
You have a functioning Shopping Cart store, but the business requirements just changed. You need to implement a coupon system that allows users to apply a percentage discount to their total.
- Modify the
CartState: Add adiscountPercentproperty (a number). - Expand the
CartActionunion: Add a new action type'APPLY_COUPON'that takes apayloadcontaining thecode(string) and thepercent(number). - Update the
cartReducer: Handle the'APPLY_COUPON'action to update thediscountPercentin the state. - Refactor the Total calculation: Ensure that the
totalreturned in the state (or calculated via a helper) accounts for thediscountPercent.
Your goal is to ensure that if you try to dispatch APPLY_COUPON without a percent value, the TypeScript compiler catches the error before you ever run the code.
There are no comments for now.