|
| 1 | +--- |
| 2 | +id: with-typescript |
| 3 | +title: Usage With TypeScript |
| 4 | +sidebar_label: Usage With TypeScript |
| 5 | +hide_title: true |
| 6 | +--- |
| 7 | + |
| 8 | +# Usage With TypeScript |
| 9 | + |
| 10 | +It is one of Redux Toolkit's design targets to offer great integration with TypeScript. |
| 11 | + |
| 12 | +This page is intended to give an overview of all common usecases and the most probable pitfall you might encounter when using RTK with TypeScript. |
| 13 | + |
| 14 | +**If you encounter any problems with the Types that are not described on this page, please open an Issue!** |
| 15 | + |
| 16 | +## Using configureStore with TypeScript |
| 17 | + |
| 18 | +Using [configureStore](../api/configureStore.ms) should not need any additional typings. You might, however, want to extract the `RootState` type and the `Dispatch` type. |
| 19 | + |
| 20 | +### Getting the `State` type |
| 21 | + |
| 22 | +The easiest way of getting the `State` type is to define the root reducer in advance and extract it's `ReturnType`. |
| 23 | +It's recommend to give it a different name like `RootState` to prevent confusion, as the type name `State` is usually overused. |
| 24 | + |
| 25 | +```typescript |
| 26 | +import { combineReducers } from '@reduxjs/toolkit' |
| 27 | +const rootReducer = combineReducers({}) |
| 28 | +export type RootState = ReturnType<typeof rootReducer> |
| 29 | +``` |
| 30 | +
|
| 31 | +### Getting the `Dispatch` type |
| 32 | +
|
| 33 | +If you want to get the `Dispatch` type from your store, you can extract it after creating the store. |
| 34 | +It's recommend to give it a different name like `AppDispatch` to prevent confusion, as the type name `Dispatch` is usually overused. |
| 35 | +
|
| 36 | +```typescript |
| 37 | +import { configureStore } from '@reduxjs/toolkit' |
| 38 | +import rootReducer from './rootReducer' |
| 39 | +const store = configureStore({ |
| 40 | + reducer: rootReducer |
| 41 | +}) |
| 42 | +export type AppDispatch = typeof store.dispatch |
| 43 | +``` |
| 44 | +
|
| 45 | +#### Extending the `Dispatch` type |
| 46 | +
|
| 47 | +By default, this `AppDispatch` type will account only for the already included `redux-thunk` middleware. If you're adding additional middlewares that provide different return types for some actions, you can overload that `AppDispatch` type. While you can just extend the `AppDispatch` type, it's recommended to do so on the store, to keep everything more consistent: |
| 48 | +
|
| 49 | +```typescript |
| 50 | +const _store = configureStore({ |
| 51 | + /* ... */ |
| 52 | +}) |
| 53 | + |
| 54 | +type EnhancedStoreType = { |
| 55 | + dispatch(action: MyCustomActionType): MyCustomReturnType |
| 56 | + dispatch(action: MyCustomActionType2): MyCustomReturnType2 |
| 57 | +} & typeof _store |
| 58 | + |
| 59 | +export const store: EnhancedStoreType = _store |
| 60 | +export type AppDispatch = typeof store.dispatch |
| 61 | +``` |
| 62 | +
|
| 63 | +#### Using the extracted `Dispatch` type with react-redux |
| 64 | +
|
| 65 | +The default `useDispatch` hook of react-redux does not contain any types that take middlewares into account. It is recommended you create your own copy of `useDispatch` that is correctly typed to your store. See [the react-redux documentation](https://react-redux.js.org/using-react-redux/static-typing#typing-the-usedispatch-hook) on this. |
| 66 | +
|
| 67 | +## createAction |
| 68 | +
|
| 69 | +For most use cases, there is no need to have a literal definition of `action.type`, so the following can be used: |
| 70 | +
|
| 71 | +```typescript |
| 72 | +createAction<number>('test') |
| 73 | +``` |
| 74 | +
|
| 75 | +This will result in the created action being of type `PayloadActionCreator<number, string>`. |
| 76 | +
|
| 77 | +In some setups, you will need a literal type for `action.type`, though. |
| 78 | +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: |
| 79 | +
|
| 80 | +```typescript |
| 81 | +createAction<number, 'test'>('test') |
| 82 | +``` |
| 83 | +
|
| 84 | +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. |
| 85 | +
|
| 86 | +```typescript |
| 87 | +function withPayloadType<T>() { |
| 88 | + return (t: T) => ({ payload: t }) |
| 89 | +} |
| 90 | +createAction('test', withPayloadType<string>()) |
| 91 | +``` |
| 92 | + |
| 93 | +### Alternative to using a literally-typed `action.type` |
| 94 | + |
| 95 | +If you are using `action.type` as discriminator on a discriminated union, for example to correctly type your payload in `case` statements, you might be interested in this alternative: |
| 96 | + |
| 97 | +Created action creators have a `match` method that acts as a [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates): |
| 98 | + |
| 99 | +```typescript |
| 100 | +const increment = createAction<number>('increment') |
| 101 | +function test(action: Action) { |
| 102 | + if (increment.match(action)) { |
| 103 | + // action.payload inferred correctly here |
| 104 | + action.payload |
| 105 | + } |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +This `match` method is also very useful in combination with `redux.observable` and rxjs's filter method. |
| 110 | + |
| 111 | +## createReducer |
| 112 | + |
| 113 | +The default way of calling `createReducer` would be with a map object, like this: |
| 114 | + |
| 115 | +```typescript |
| 116 | +createReducer(0, { |
| 117 | + increment: (state, action: PayloadAction<number>) => state + action.payload |
| 118 | +}) |
| 119 | +``` |
| 120 | + |
| 121 | +Unfortunately, as the keys are only strings, using that API TypeScript can neither infer nor validate the action types for you: |
| 122 | + |
| 123 | +```typescript |
| 124 | +{ |
| 125 | + const increment = createAction<number, 'increment'>('increment') |
| 126 | + const decrement = createAction<number, 'decrement'>('decrement') |
| 127 | + createReducer(0, { |
| 128 | + [increment.type]: (state, action) => { |
| 129 | + // action is any here |
| 130 | + }, |
| 131 | + [decrement.type]: (state, action: PayloadAction<string>) => { |
| 132 | + // even though action should actually be PayloadAction<number>, TypeScript can't detect that and won't give a warning here. |
| 133 | + } |
| 134 | + }) |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +This is why we're offering an alternative API. |
| 139 | + |
| 140 | +### Recommendation for TS users: the alternative builder API |
| 141 | + |
| 142 | +Instead of using a Map Object as an argument to `createReducer`, you can also use a callback that receives a `ActionReducerMapBuilder` object: |
| 143 | + |
| 144 | +```typescript |
| 145 | +{ |
| 146 | + const increment = createAction<number, 'increment'>('increment') |
| 147 | + const decrement = createAction<number, 'decrement'>('decrement') |
| 148 | + createReducer(0, builder => |
| 149 | + builder |
| 150 | + .add(increment, (state, action) => { |
| 151 | + // action is inferred correctly here |
| 152 | + }) |
| 153 | + .add(decrement, (state, action: PayloadAction<string>) => { |
| 154 | + // this would error out |
| 155 | + }) |
| 156 | + ) |
| 157 | +} |
| 158 | +``` |
| 159 | + |
| 160 | +# createSlice |
| 161 | + |
| 162 | +As `createSlice` creates your actions as well as your reducer for you, you don't have to worry about type safety here. |
| 163 | +Action types can just be provided inline: |
| 164 | + |
| 165 | +```typescript |
| 166 | +{ |
| 167 | + const slice = createSlice({ |
| 168 | + name: 'test', |
| 169 | + initialState: 0, |
| 170 | + reducers: { |
| 171 | + increment: (state, action: PayloadAction<number>) => |
| 172 | + state + action.payload |
| 173 | + } |
| 174 | + }) |
| 175 | + // now available: |
| 176 | + slice.actions.increment(2) |
| 177 | + // also available: |
| 178 | + slice.caseReducers.increment(0, { type: 'increment', payload: 5 }) |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +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`: |
| 183 | + |
| 184 | +```typescript |
| 185 | +type State = number |
| 186 | +const increment: CaseReducer<State, PayloadAction<number>> = (state, action) => |
| 187 | + state + action.payload |
| 188 | + |
| 189 | +createSlice({ |
| 190 | + name: 'test', |
| 191 | + initialState: 0, |
| 192 | + reducers: { |
| 193 | + increment |
| 194 | + } |
| 195 | +}) |
| 196 | +``` |
| 197 | + |
| 198 | +### On the "type" property of slice action Reducers |
| 199 | + |
| 200 | +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. |
| 201 | + |
| 202 | +In most cases that type would be required as a literal, the `slice.action.myAction.match` [type predicate](https://www.typescriptlang.org/docs/handbook/advanced-types.html#using-type-predicates) should prove as a viable alternative: |
| 203 | + |
| 204 | +```typescript |
| 205 | +const slice = createSlice({ |
| 206 | + name: 'test', |
| 207 | + initialState: 0, |
| 208 | + reducers: { |
| 209 | + increment: (state, action: PayloadAction<number>) => state + action.payload |
| 210 | + } |
| 211 | +}) |
| 212 | + |
| 213 | +function myCustomMiddleware(action: Action) { |
| 214 | + if (slice.actions.increment.match(action)) { |
| 215 | + // `action` is narrowed down to the type `PayloadAction<number>` here. |
| 216 | + } |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +If you actually _need_ that type, unfortunately there is no other way than manual casting. |
| 221 | + |
| 222 | +### Type safety with extraReducers |
| 223 | + |
| 224 | +Like in `createReducer`, the `extraReducers` map object is not easy to fully type. So, like with `createReducer`, you can also use a _builder callback_ style for that. (See example above.) |
0 commit comments