Использование с Typescript
Redux Toolkit is written in TypeScript, and its API is designed to enable great integration with TypeScript applications.
This page is intended to give an overview of all common usecases and the most probable pitfalls you might encounter when using RTK with TypeScript.
If you encounter any problems with the types that are not described on this page, please open an issue for discussion.
Using configureStore
with TypeScript
configureStore
with TypeScriptUsing configureStore should not need any additional typings. You might, however, want to extract the RootState
type and the Dispatch
type.
Getting the State
type
State
typeThe easiest way of getting the State
type is to define the root reducer in advance and extract its ReturnType
.
It is recommend to give the type a different name like RootState
to prevent confusion, as the type name State
is usually overused.
```typescript {3} import { combineReducers } from '@reduxjs/toolkit' const rootReducer = combineReducers({}) export type RootState = ReturnType
Getting the Dispatch
type
Dispatch
typeIf you want to get the Dispatch
type from your store, you can extract it after creating the store. It is recommended to give the type a different name like AppDispatch
to prevent confusion, as the type name Dispatch
is usually overused. You may also find it to be more convenient to export a hook like useAppDispatch
shown below, then using it wherever you'd call useDispatch
.
```typescript {6} import { configureStore } from '@reduxjs/toolkit' import { useDispatch } from 'react-redux' import rootReducer from './rootReducer'
const store = configureStore({ reducer: rootReducer })
export type AppDispatch = typeof store.dispatch export const useAppDispatch = () => useDispatch() // Export a hook that can be reused to resolve types
Using MiddlewareArray
without getDefaultMiddleware
MiddlewareArray
without getDefaultMiddleware
If you want to skip the usage of getDefaultMiddleware
altogether, you can still use MiddlewareArray
for type-safe concatenation of your middleware
array. This class extends the default JavaScript Array
type, only with modified typings for .concat(...)
and the additional .prepend(...)
method.
This is generally not required though, as you will probably not run into any array-type-widening issues as long as you are using as const
and do not use the spread operator.
So the following two calls would be equivalent:
Using the extracted Dispatch
type with React-Redux
Dispatch
type with React-ReduxBy default, the React-Redux useDispatch
hook does not contain any types that take middlewares into account. If you need a more specific type for the dispatch
function when dispatching, you may specify the type of the returned dispatch
function, or create a custom-typed version of useSelector
. See the React-Redux documentation for details.
createAction
createAction
For most use cases, there is no need to have a literal definition of action.type
, so the following can be used:
This will result in the created action being of type PayloadActionCreator<number, string>
.
In some setups, you will need a literal type for action.type
, though. Unfortunately, TypeScript type definitions do not allow for a mix of manually-defined and inferred type parameters, so you'll have to specify the type
both in the Generic definition as well as in the actual JavaScript code:
If you are looking for an alternate way of writing this without the duplication, you can use a prepare callback so that both type parameters can be inferred from arguments, removing the need to specify the action type.
Alternative to using a literally-typed action.type
action.type
If you are using action.type
as a discriminator on a discriminated union, for example to correctly type your payload in case
statements, you might be interested in this alternative:
Created action creators have a match
method that acts as a type predicate:
This match
method is also very useful in combination with redux-observable
and RxJS's filter
method.
createReducer
createReducer
The default way of calling createReducer
would be with a "lookup table" / "map object", like this:
Unfortunately, as the keys are only strings, using that API TypeScript can neither infer nor validate the action types for you:
As an alternative, RTK includes a type-safe reducer builder API.
Building Type-Safe Reducer Argument Objects
Instead of using a simple object as an argument to createReducer
, you can also use a callback that receives a ActionReducerMapBuilder
instance:
```typescript {3-10} const increment = createAction('increment') const decrement = createAction('decrement') createReducer(0, builder => builder .addCase(increment, (state, action) => { // action is inferred correctly here }) .addCase(decrement, (state, action: PayloadAction) => { // this would error out }) )
createSlice
createSlice
As createSlice
creates your actions as well as your reducer for you, you don't have to worry about type safety here. Action types can just be provided inline:
If you have too many reducers and defining them inline would be messy, you can also define them outside the createSlice
call and type them as CaseReducer
:
Defining the Initial State Type
You might have noticed that it is not a good idea to pass your SliceState
type as a generic to createSlice
. This is due to the fact that in almost all cases, follow-up generic parameters to createSlice
need to be inferred, and TypeScript cannot mix explicit declaration and inference of generic types within the same "generic block".
The standard approach is to declare an interface or type for your state, create an initial state value that uses that type, and pass the initial state value to createSlice
. You can also use the construct initialState: myInitialState as SliceState
.
```ts {1,4,8,15} type SliceState = { state: 'loading' } | { state: 'finished'; data: string }
// First approach: define the initial state using that type const initialState: SliceState = { state: 'loading' }
createSlice({ name: 'test1', initialState, // type SliceState is inferred for the state of the slice reducers: {} })
// Or, cast the initial state as necessary createSlice({ name: 'test2', initialState: { state: 'loading' } as SliceState, reducers: {} })
Generated Action Types for Slices
As TS cannot combine two string literals (slice.name
and the key of actionMap
) into a new literal, all actionCreators created by createSlice
are of type 'string'. This is usually not a problem, as these types are only rarely used as literals.
In most cases that type
would be required as a literal, the slice.action.myAction.match
type predicate should be a viable alternative:
```ts {10} const slice = createSlice({ name: 'test', initialState: 0, reducers: { increment: (state, action: PayloadAction) => state + action.payload } })
function myCustomMiddleware(action: Action) { if (slice.actions.increment.match(action)) { // action
is narrowed down to the type PayloadAction<number>
here. } }
Like the builder
in createReducer
, this builder
also accepts addMatcher
(see typing builder.matcher
) and addDefaultCase
.
Wrapping createSlice
createSlice
If you need to reuse reducer logic, it is common to write "higher-order reducers" that wrap a reducer function with additional common behavior. This can be done with createSlice
as well, but due to the complexity of the types for createSlice
, you have to use the SliceCaseReducers
and ValidateSliceCaseReducers
types in a very specific way.
Here is an example of such a "generic" wrapped createSlice
call:
createAsyncThunk
createAsyncThunk
In the most common use cases, you should not need to explicitly declare any types for the createAsyncThunk
call itself.
Just provide a type for the first argument to the payloadCreator
argument as you would for any function argument, and the resulting thunk will accept the same type as its input parameter. The return type of the payloadCreator
will also be reflected in all generated action types.
```ts {8,11,18} interface MyData { // ... }
const fetchUserById = createAsyncThunk( 'users/fetchById', // Declare the type your function argument here: async (userId: number) => { const response = await fetch(https://reqres.in/api/users/${userId}
) // Inferred return type: Promise return (await response.json()) as MyData } )
// the parameter of fetchUserById
is automatically inferred to number
here // and dispatching the resulting thunkAction will return a Promise of a correctly // typed "fulfilled" or "rejected" action. const lastReturnedAction = await store.dispatch(fetchUserById(3))
If you are performing a request that you know will typically either be a success or have an expected error format, you can pass in a type to rejectValue
and return rejectWithValue(knownPayload)
in the action creator. This allows you to reference the error payload in the reducer as well as in a component after dispatching the createAsyncThunk
action.
While this notation for state
, dispatch
, extra
and rejectValue
might seem uncommon at first, it allows you to provide only the types for these you actually need - so for example, if you are not accessing getState
within your payloadCreator
, there is no need to provide a type for state
. The same can be said about rejectValue
- if you don't need to access any potential error payload, you can ignore it.
In addition, you can leverage checks against action.payload
and match
as provided by createAction
as a type-guard for when you want to access known properties on defined types. Example:
In a reducer
In a component
createEntityAdapter
createEntityAdapter
Typing createEntityAdapter
only requires you to specify the entity type as the single generic argument.
The example from the createEntityAdapter
documentation would look like this in TypeScript:
```ts {7} interface Book { bookId: number title: string // ... }
const booksAdapter = createEntityAdapter({ selectId: book => book.bookId, sortComparer: (a, b) => a.title.localeCompare(b.title) })
const booksSlice = createSlice({ name: 'books', initialState: booksAdapter.getInitialState(), reducers: { bookAdded: booksAdapter.addOne, booksReceived(state, action: PayloadAction<{ books: Book[] }>) { booksAdapter.setAll(state, action.payload.books) } } })
The methods addMany
, upsertMany
, and setAll
all allow you to pass in the entities
portion of this directly with no extra conversion steps. However, the normalizr
TS typings currently do not correctly reflect that multiple data types may be included in the results, so you will need to specify that type structure yourself.
Here is an example of how that would look:
Last updated