Skip to content

Commit 456eabb

Browse files
committed
add "Usage with TypeScript" section to the docs
1 parent 5eb1cc7 commit 456eabb

File tree

2 files changed

+225
-1
lines changed

2 files changed

+225
-1
lines changed

docs/usage/with-typescript.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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.)

website/sidebars.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"tutorials/intermediate-tutorial",
77
"tutorials/advanced-tutorial"
88
],
9-
"Using Redux Toolkit": ["usage/usage-guide"],
9+
"Using Redux Toolkit": ["usage/usage-guide", "usage/with-typescript"],
1010
"API Reference": [
1111
"api/configureStore",
1212
"api/getDefaultMiddleware",

0 commit comments

Comments
 (0)