Guide Redux Toolkit with TypeScript

Master type-safe state management with Redux Toolkit. Learn typed hooks, createSlice patterns, async operations, and RTK Query for production-ready React applications.

Why Redux Toolkit with TypeScript

Redux Toolkit is the official, recommended way to write Redux logic. It simplifies common Redux concerns including boilerplate, configuration, and middleware. Combined with TypeScript, it provides excellent type safety for global state management for your web applications.

Redux Toolkit was created to address three common concerns about Redux:

  • "Configuring a Redux store is too complicated"
  • "I have to add a lot of packages to get Redux to do anything useful"
  • "Redux requires too much boilerplate code"

According to the official Redux Toolkit documentation, these pain points led to the creation of RTK as a batteries-included solution that provides sensible defaults out of the box.

Redux Toolkit vs Redux

The key difference is that Redux Toolkit provides sensible defaults and built-in utilities that eliminate the boilerplate while maintaining full type safety. RTK uses Immer internally, allowing you to write "mutating" logic that gets converted to immutable updates.

TypeScript Benefits in Redux

Why type safety matters for production applications

Compile-Time Error Detection

Catch type mismatches before runtime, preventing production bugs in state management logic.

Autocomplete Support

Get intelligent suggestions for state properties, action types, and dispatch methods.

Refactoring Confidence

Safely rename properties and restructure state knowing the compiler will catch breaking changes.

Self-Documenting Code

Types serve as living documentation for state shape and action structures.

Installation and Setup

Package Installation

Install both Redux Toolkit and React bindings:

npm install @reduxjs/toolkit react-redux
# or
yarn add @reduxjs/toolkit react-redux

As documented in the Redux Toolkit Getting Started guide, these two packages provide everything you need to start using Redux with React.

TypeScript Version Requirements

Redux Toolkit follows DefinitelyTyped's policy of supporting TypeScript versions released within the past two years. As of RTK 2.x, this means:

  • RTK 2.x requires TypeScript 5.4+
  • RTK 1.9.x requires TypeScript 4.7+

If you're unable to upgrade TypeScript, RTK may still work with older versions, but you may encounter type errors or missing type inference. The TypeScript usage documentation provides detailed information on version compatibility.

For optimal type safety and the best developer experience, always use the minimum required TypeScript version or higher. When building modern React applications, having the latest TypeScript version ensures you benefit from improved type inference and language features.

Configuring the Store with TypeScript
1import { configureStore } from '@reduxjs/toolkit'2import rootReducer from './reducers'3 4const store = configureStore({5 reducer: rootReducer6})7 8export type RootState = ReturnType<typeof store.getState>9export type AppDispatch = typeof store.dispatch10 11// Typed hooks for React components12export const useAppDispatch = () => useDispatch<AppDispatch>()13export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

Typed Dispatch and Hooks

Creating typed useDispatch and useSelector hooks is essential for type-safe React-Redux integration:

import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'

export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector

These typed hooks ensure that:

  • Dispatch knows exactly what actions can be dispatched
  • Selector knows the complete state shape and provides autocomplete
  • Components receive proper type checking for Redux interactions

As outlined in the Redux Toolkit TypeScript documentation, this pattern prevents runtime errors by catching type mismatches at compile time.

Creating Type-Safe Slices with createSlice
1import { createSlice, PayloadAction } from '@reduxjs/toolkit'2 3interface CounterState {4 value: number5}6 7const initialState: CounterState = {8 value: 09}10 11const counterSlice = createSlice({12 name: 'counter',13 initialState,14 reducers: {15 increment(state) {16 state.value += 117 },18 decrement(state) {19 state.value -= 120 },21 incrementByAmount(state, action: PayloadAction<number>) {22 state.value += action.payload23 }24 }25})26 27export const { increment, decrement, incrementByAmount } = counterSlice.actions28export default counterSlice.reducer

Creating Type-Safe Slices

Using createSlice

The createSlice API automatically generates action creators and action types with proper TypeScript support. The key is using PayloadAction<T> for actions that carry data:

incrementByAmount(state, action: PayloadAction<number>) {
 state.value += action.payload
}

This typing ensures:

  • action.payload is typed as number
  • TypeScript will error if you try to assign a non-number
  • IDE autocomplete suggests the correct payload type

According to the Redux Toolkit TypeScript patterns documentation, this approach provides complete type safety for slice reducers.

Defining State and Action Types

Best practice is to:

  1. Define a TypeScript interface for your slice state
  2. Use it as the type for initialState
  3. Export the state interface for use in selectors
  4. Use PayloadAction<PayloadType> for reducer arguments

By following these patterns in your React web development projects, you ensure maintainable and type-safe state management across your application.

Async Operations with createAsyncThunk
1import { createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'2 3interface User {4 id: number5 name: string6}7 8interface UserResponse {9 users: User[]10}11 12// Async thunk with typed generics:13// ReturnType, ArgumentType, ThunkApiConfig14export const fetchUsers = createAsyncThunk<15 UserResponse,16 void,17 { rejectValue: string }18>(19 'users/fetchUsers',20 async (_, { rejectWithValue }) => {21 try {22 const response = await fetch('/api/users')23 if (!response.ok) {24 return rejectWithValue('Failed to fetch users')25 }26 return await response.json()27 } catch (error) {28 return rejectWithValue(29 error instanceof Error ? error.message : 'Unknown error'30 )31 }32 }33)

Async Operations with createAsyncThunk

Async Thunk Pattern

createAsyncThunk simplifies async logic with three generic parameters:

  1. ReturnType: What the async function returns on success
  2. ArgumentType: What the thunk receives when dispatched
  3. ThunkApiConfig: Configuration including rejectValue type
createAsyncThunk<
 UserResponse, // Return type on success
 void, // Argument type
 { rejectValue: string } // Config with error type
>

As described in the Redux Toolkit TypeScript documentation, these generics ensure complete type safety throughout the async operation lifecycle.

Handling Async States

Use extraReducers with the builder callback pattern to handle the lifecycle:

const userSlice = createSlice({
 name: 'users',
 initialState,
 reducers: {},
 extraReducers: (builder) => {
 builder
 .addCase(fetchUsers.pending, (state) => {
 state.loading = true
 state.error = null
 })
 .addCase(fetchUsers.fulfilled, (state, action) => {
 state.loading = false
 state.users = action.payload.users
 })
 .addCase(fetchUsers.rejected, (state, action) => {
 state.loading = false
 state.error = action.payload || 'Unknown error'
 })
 }
})
Memoized Selectors with createSelector
1import { createSelector } from '@reduxjs/toolkit'2import { RootState } from './store'3 4interface CartItem {5 id: string6 price: number7 quantity: number8}9 10interface CartState {11 items: CartItem[]12}13 14// Base selector15const selectCartItems = (state: RootState): CartItem[] => 16 state.cart.items17 18// Memoized derived selector19export const selectCartTotal = createSelector(20 [selectCartItems],21 (items) => items.reduce(22 (total, item) => total + item.price * item.quantity, 23 024 )25)26 27// Another memoized selector28export const selectCartItemCount = createSelector(29 [selectCartItems],30 (items) => items.reduce((count, item) => count + item.quantity, 0)31)

Memoized Selectors with createSelector

Performance Optimization

createSelector from the Reselect library (re-exported by RTK) creates memoized selectors that only recompute when their input selectors return different values:

export const selectCartTotal = createSelector(
 [selectCartItems],
 (items) => items.reduce((total, item) => total + item.price * item.quantity, 0)
)

Without memoization:

  • Every render recalculates derived values
  • All components using the selector re-render

With memoization:

  • Only recomputes when cart.items actually changes
  • Prevents needless re-renders, improving performance

The Redux Toolkit selector patterns demonstrate how createSelector optimizes derived data calculations.

RTK Query with TypeScript
1import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'2 3interface Post {4 id: number5 title: string6 body: string7}8 9export const postsApi = createApi({10 reducerPath: 'postsApi',11 baseQuery: fetchBaseQuery({ baseUrl: '/api' }),12 endpoints: (builder) => ({13 getPosts: builder.query<Post[], void>({14 query: () => 'posts'15 }),16 getPostById: builder.query<Post, number>({17 query: (id) => `posts/${id}`18 }),19 createPost: builder.mutation<Post, Partial<Post>>({20 query: (newPost) => ({21 url: 'posts',22 method: 'POST',23 body: newPost24 })25 })26 })27})28 29// Auto-generated hooks with full TypeScript support30export const {31 useGetPostsQuery,32 useGetPostByIdQuery,33 useCreatePostMutation34} = postsApi

RTK Query with TypeScript

Data Fetching Made Simple

RTK Query is a powerful data fetching and caching solution included in Redux Toolkit. Using TypeScript with createApi provides excellent type safety:

Endpoint Typing:

  • builder.query<ResultType, QueryArg> for GET requests
  • builder.mutation<ResultType, BodyType> for POST/PUT/DELETE

Type Inference:

  • TypeScript automatically infers hook return types
  • Query arguments are properly typed
  • Error types are inferred from the API configuration

As detailed in the RTK Query TypeScript documentation, this approach eliminates the need for manual type definitions while maintaining complete type safety. For applications requiring efficient API integrations, RTK Query provides a robust solution that handles caching, polling, and optimistic updates out of the box.

Using Typed Hooks in Components
1// Counter component with typed Redux hooks2import { useAppSelector, useAppDispatch } from './hooks'3import { increment } from './counterSlice'4 5function Counter() {6 const count = useAppSelector((state) => state.counter.value)7 const dispatch = useAppDispatch()8 9 return (10 <button onClick={() => dispatch(increment())}>11 Count: {count}12 </button>13 )14}15 16// Posts component with RTK Query hooks17import { useGetPostsQuery } from './services/postsApi'18 19function PostsList() {20 const { data, error, isLoading } = useGetPostsQuery()21 22 if (isLoading) return <div>Loading...</div>23 if (error) return <div>Error loading posts</div>24 25 return (26 <ul>27 {data?.map((post) => (28 <li key={post.id}>{post.title}</li>29 ))}30 </ul>31 )32}

Best Practices and Common Patterns

Organizing Slice Files

Recommended structure for scalable Redux applications:

src/
 features/
 counter/
 counterSlice.ts # Slice with actions & reducer
 counterSelectors.ts # Custom selectors
 counterTypes.ts # Type definitions
 store/
 store.ts # Store configuration
 hooks.ts # Typed React-Redux hooks

Type Safety Throughout the App

  1. Export types from slices for reuse
  2. Use RootState for all selector functions
  3. Create typed hooks in a dedicated hooks file
  4. Avoid any types - be explicit about state shape

Avoiding Common TypeScript Pitfalls

  • Circular references: Define types in separate files when needed
  • Missing exports: Export action types for use in components
  • Generic inference: Be explicit with createAsyncThunk generics
  • Strict null checks: Account for optional state properties

Testing Type-Safe Redux Code

Type safety doesn't replace testing, but it catches many errors early:

  • Test reducer logic with known state and actions
  • Test async thunks with mocked API responses
  • Test selectors with various input states
  • Test component integration with typed hooks

Frequently Asked Questions

Do I need TypeScript to use Redux Toolkit?

No, Redux Toolkit works without TypeScript. However, TypeScript provides significant benefits including compile-time error detection, autocomplete support, and refactoring confidence. Since RTK is written in TypeScript, you get excellent type inference even in JavaScript projects.

When should I use createSelector?

Use createSelector when you need to derive data from the store and want to avoid recalculating on every render. It's especially valuable for expensive computations like filtering, sorting, or aggregating data from large state objects.

RTK Query vs createAsyncThunk - which should I use?

Use RTK Query for server state (API data, caching, synchronization). Use createAsyncThunk for client-side async operations that don't fit the REST pattern. Many applications use both - RTK Query for data fetching and createAsyncThunk for operations like form submissions.

How do I type complex state with multiple slices?

Combine slice types using TypeScript's intersection types. Define each slice's state type, then create a RootState type that combines them. Export each type from its slice file for reusability in selectors and components.

Can I use Redux Toolkit with React Server Components?

Yes, but with considerations. Client components can use hooks normally. For server components, you'll need to access the store differently or use context-based patterns. Next.js has specific recommendations for Redux integration.

Conclusion

Redux Toolkit with TypeScript provides a powerful, type-safe foundation for state management in React applications. Key takeaways:

PatternPurpose
configureStoreTyped store setup with middleware
Typed hooksType-safe useDispatch and useSelector
PayloadAction<T>Typed action payloads in reducers
createAsyncThunkAsync operations with type safety
createSelectorMemoized derived state
createApiType-safe data fetching with RTK Query

Start with typed hooks and createSlice for basic state management. Add RTK Query for data fetching needs. Use createAsyncThunk for complex async workflows that don't fit RTK Query's patterns.

For comprehensive documentation, refer to the official Redux Toolkit Getting Started guide, TypeScript usage patterns, and RTK Query TypeScript integration.

Implementing these patterns in your React applications ensures maintainable, type-safe state management that scales with your project.

Ready to Build Type-Safe React Applications?

Our team specializes in modern React development with Redux Toolkit and TypeScript. Let's discuss how we can help scale your application's state management.