Skip to content

Commit 9997122

Browse files
authored
Rework "Usage with TS" page with more details (#3633)
1 parent 17f0ea0 commit 9997122

File tree

1 file changed

+140
-52
lines changed

1 file changed

+140
-52
lines changed

docs/recipes/UsageWithTypescript.md

Lines changed: 140 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,29 @@ hide_title: true
77

88
# Usage with TypeScript
99

10+
## Overview
11+
1012
**TypeScript** is a typed superset of JavaScript. It has become popular recently in applications due to the benefits it can bring. If you are new to TypeScript it is highly recommended to become familiar with it first before proceeding. You can check out its documentation [here.](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html)
1113

1214
TypeScript has the potential to bring the following benefits to a Redux application:
1315

14-
1. Type safety for reducers, state and action creators
16+
1. Type safety for reducers, state and action creators, and UI components
1517
2. Easy refactoring of typed code
1618
3. A superior developer experience in a team environment
1719

20+
## Notes & Considerations
21+
22+
- While we do [officially recommend use of static typing with Redux](../style-guide/style-guide.md#use-static-typing), use of TypeScript does have tradeoffs in terms of setup, amount of code written, and readability. TypeScript will likely provide a net benefit in larger apps or codebases need to be maintained over time by many people, but may feel like too much overhead in smaller projects. Take time to evaluate the tradeoffs and decide whether it's worth using TS in your own application.
23+
- This page primarily covers adding type checking for the Redux core, and only gives shorter examples of using TS with other Redux libraries. See their respective documentation for further details.
24+
- There are multiple possible approaches to type checking Redux code. This page demonstrates some of the common and recommended approaches to keep things simple, and is not an exhaustive guide
25+
1826
## A Practical Example
1927

2028
We will be going through a simplistic chat application to demonstrate a possible approach to include static typing. This chat application will have two reducers. The _chat reducer_ will focus on storing the chat history and the _system reducer_ will focus on storing session information.
2129

2230
The full source code is available on [codesandbox here](https://codesandbox.io/s/w02m7jm3q7). Note that by going through this example yourself you will experience some of the benefits of using TypeScript.
2331

24-
## Type Checking State
32+
### Type Checking State
2533

2634
Adding types to each slice of state is a good place to start since it does not rely on other types. In this example we start by describing the chat reducer's slice of state:
2735

@@ -53,7 +61,7 @@ export interface SystemState {
5361

5462
Note that we are exporting these interfaces to reuse them later in reducers and action creators.
5563

56-
## Type Checking Actions & Action Creators
64+
### Type Checking Actions & Action Creators
5765

5866
We will be using string literals and using `typeof` to declare our action constants and infer types. Note that we are making a tradeoff here when we declare our types in a separate file. In exchange for separating our types into a separate file, we get to keep our other files more focused on their purpose. While this tradeoff can improve the maintainability of the codebase, it is perfectly fine to organize your project however you see fit.
5967

@@ -136,7 +144,7 @@ export function updateSession(newSession: SystemState): SystemActionTypes {
136144
}
137145
```
138146

139-
## Type Checking Reducers
147+
### Type Checking Reducers
140148

141149
Reducers are just pure functions that take the previous state, an action and then return the next state. In this example, we explicitly declare the type of actions this reducer will receive along with what it should return (the appropriate slice of state). With these additions TypeScript will give rich intellisense on the properties of our actions and state. In addition, we will also get errors when a certain case does not return the `ChatState`.
142150

@@ -212,7 +220,7 @@ export function systemReducer(
212220
}
213221
```
214222

215-
We now need to generate the root reducer function, which is normally done using `combineReducers`. Note that we do not have to explicitly declare a new interface for AppState. We can use `ReturnType` to infer state shape from the `rootReducer`.
223+
We now need to generate the root reducer function, which is normally done using `combineReducers`. Note that we do not have to explicitly declare a new interface for RootState. We can use `ReturnType` to infer state shape from the `rootReducer`.
216224

217225
```ts
218226
// src/store/index.ts
@@ -225,87 +233,99 @@ const rootReducer = combineReducers({
225233
chat: chatReducer
226234
})
227235

228-
export type AppState = ReturnType<typeof rootReducer>
236+
export type RootState = ReturnType<typeof rootReducer>
229237
```
230238
231239
## Usage with React Redux
232240
233-
While React Redux is a separate library from redux itself, it is commonly used with react. For this reason, we will go through how React Redux works with TypeScript using the same example used previously in this section.
241+
While React Redux is a separate library from Redux itself, it is commonly used with React.
234242
235-
Note: React Redux does not have type checking by itself, you will have to install `@types/react-redux` by running `npm i @types/react-redux -D`.
243+
For a complete guide on how to correctly use React-Redux with TypeScript, see **[the "Static Typing" page in the React-Redux docs](https://react-redux.js.org/using-react-redux/static-typing)**. This section will highlight the standard patterns.
236244
237-
We will now add type checking to the parameter that `mapStateToProps` receives. Luckily, we have already declared what the store should look like from defining a type that infers from the `rootReducer`:
245+
React-Redux doesn't ship with its own type definitions. If you are using Typescript you should install the [`@types/react-redux` type definitions](https://npm.im/@types/react-redux) from npm. In addition to typing the library functions, the types also export some helpers to make it easier to write typesafe interfaces between your Redux store and your React components.
246+
247+
### Typing the useSelector hook
248+
249+
Declare the type of the `state` parameter in the selector function, and the return type of `useSelector` will be inferred to match the return type of the selector:
238250
239251
```ts
240-
// src/App.tsx
252+
interface RootState {
253+
isOn: boolean
254+
}
241255

242-
import { AppState } from './store'
256+
// TS infers type: (state: RootState) => boolean
257+
const selectIsOn = (state: RootState) => state.isOn
243258

244-
const mapStateToProps = (state: AppState) => ({
245-
system: state.system,
246-
chat: state.chat
247-
})
259+
// TS infers `isOn` is boolean
260+
const isOn = useSelector(selectIsOn)
248261
```
249262

250-
In this example we declared two different properties in `mapStateToProps`. To type check these properties, we will create an interface with the appropriate slices of state:
263+
### Typing the `useDispatch` hook
264+
265+
By default, the return value of `useDispatch` is the standard `Dispatch` type defined by the Redux core types, so no declarations are needed:
251266

252267
```ts
253-
// src/App.tsx
268+
const dispatch = useDispatch()
269+
```
254270

255-
import { SystemState } from './store/system/types'
271+
### Typing the `connect` higher order component
256272

257-
import { ChatState } from './store/chat/types'
273+
Use the `ConnectedProps<T>` type exported by `@types/react-redux^7.1.2` to infer the types of the props from `connect` automatically. This requires splitting the `connect(mapState, mapDispatch)(MyComponent)` call into two parts:
258274

259-
interface AppProps {
260-
chat: ChatState
261-
system: SystemState
262-
}
263-
```
275+
```tsx
276+
import { connect, ConnectedProps } from 'react-redux'
264277

265-
We can now use this interface to specify what props the appropriate component will receive like so:
278+
interface RootState {
279+
isOn: boolean
280+
}
266281

267-
```ts
268-
// src/App.tsx
282+
const mapState = (state: RootState) => ({
283+
isOn: state.isOn
284+
})
269285

270-
class App extends React.Component<AppProps> {
271-
```
286+
const mapDispatch = {
287+
toggleOn: () => ({ type: 'TOGGLE_IS_ON' })
288+
}
272289

273-
In this component we are also mapping action creators to be available in the component's props. In the same `AppProps` interface we will use the powerful `typeof` feature to let TypeScript know what our action creators expect like so:
290+
const connector = connect(
291+
mapState,
292+
mapDispatch
293+
)
274294

275-
```ts
276-
// src/App.tsx
295+
// The inferred type will look like:
296+
// {isOn: boolean, toggleOn: () => void}
297+
type PropsFromRedux = ConnectedProps<typeof connector>
277298

278-
import { SystemState } from './store/system/types'
279-
import { updateSession } from './store/system/actions'
299+
type Props = PropsFromRedux & {
300+
backgroundColor: string
301+
}
280302

281-
import { ChatState } from './store/chat/types'
282-
import { sendMessage } from './store/chat/actions'
303+
const MyComponent = (props: Props) => (
304+
<div style={{ backgroundColor: props.backgroundColor }}>
305+
<button onClick={props.toggleOn}>
306+
Toggle is {props.isOn ? 'ON' : 'OFF'}
307+
</button>
308+
</div>
309+
)
283310

284-
interface AppProps {
285-
sendMessage: typeof sendMessage
286-
updateSession: typeof updateSession
287-
chat: ChatState
288-
system: SystemState
289-
}
311+
export default connector(MyComponent)
290312
```
291313

292-
With these additions made props that come from redux's side are now being type checked. Feel free to extend the interface as necessary to account for additional props being passed down from parent components.
293-
294314
## Usage with Redux Thunk
295315

296-
Redux Thunk is a commonly used middleware for asynchronous orchestration. Feel free to check out its documentation [here](https://github.com/reduxjs/redux-thunk). A thunk is a function that returns another function that takes parameters `dispatch` and `getState`. Redux Thunk has a built in type `ThunkAction` which we can utilize like so:
316+
Redux Thunk is a commonly used middleware for writing sync and async logic that interacts with the Redux store. Feel free to check out its documentation [here](https://github.com/reduxjs/redux-thunk). A thunk is a function that returns another function that takes parameters `dispatch` and `getState`. Redux Thunk has a built in type `ThunkAction` which we can use to define types for those arguments:
297317

298318
```ts
299319
// src/thunks.ts
300320

301321
import { Action } from 'redux'
302322
import { sendMessage } from './store/chat/actions'
303-
import { AppState } from './store'
323+
import { RootState } from './store'
304324
import { ThunkAction } from 'redux-thunk'
305325

306326
export const thunkSendMessage = (
307327
message: string
308-
): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
328+
): ThunkAction<void, RootState, null, Action<string>> => async dispatch => {
309329
const asyncResp = await exampleAPI()
310330
dispatch(
311331
sendMessage({
@@ -321,11 +341,79 @@ function exampleAPI() {
321341
}
322342
```
323343

344+
To reduce repetition, you might want to define a reusable `AppThunk` type once, in your store file, and then use that type whenever you write a thunk:
345+
346+
```ts
347+
export type AppThunk<ReturnType = void> = ThunkAction<
348+
ReturnType,
349+
RootState,
350+
null,
351+
Action<string>
352+
>
353+
```
354+
324355
It is highly recommended to use action creators in your dispatch since we can reuse the work that has already been done to type check these functions.
325356
326-
## Notes & Considerations
357+
## Usage with Redux Toolkit
358+
359+
The official [Redux Toolkit](https://redux-toolkit.js.org) package is written in TypeScript, and provides APIs that are designed to work well in TypeScript applications.
360+
361+
### Typing `configureStore`
362+
363+
`configureStore` infers the type of the state value from the provided root reducer function, so no specific type declarations should be needed. However, you may want to export the type of `store.dispatch`, which should already have the Thunk middleware types included:
364+
365+
```ts
366+
const store = configureStore({
367+
reducer: rootReducer
368+
})
369+
370+
export type AppDispatch = typeof store.dispatch
371+
```
372+
373+
### Typing `createAction`
374+
375+
`createAction` requires that the type of the payload be explicitly defined, unless there is no payload required:
376+
377+
```ts
378+
const add = createAction<number>('add')
379+
```
380+
381+
### Typing `createReducer`
382+
383+
`createReducer` will infer the type of its state value from the `initialState` argument. Action types should be declared explicitly:
384+
385+
```ts
386+
const initialState: number = 0
387+
const counterReducer = createReducer(initialState, {
388+
increment: (state, action: PayloadAction<number>) => state + action.payload
389+
})
390+
```
391+
392+
### Typing `createSlice`
393+
394+
Similar to `createReducer`, `createSlice` will infer the type of its state value from the `initialState` argument. Action types should be declared explicitly, and will be reused for the generated action creators:
395+
396+
```ts
397+
const counterSlice = createSlice({
398+
name: 'counter',
399+
initialState: 0 as number,
400+
reducers: {
401+
increment(state, action: PayloadAction<number>) {
402+
return state + action.payload
403+
}
404+
}
405+
})
406+
```
407+
408+
## Resources
409+
410+
For further information, see these additional resources:
327411

328-
- This documentation covers primarily the redux side of type checking. For demonstration purposes, the codesandbox example also uses react with React Redux to demonstrate an integration.
329-
- There are multiple approaches to type checking redux, this is just one of many approaches.
330-
- This example only serves the purpose of showing this approach, meaning other advanced concepts have been stripped out to keep things simple. If you are code splitting your redux take a look at [this post](https://medium.com/@matthewgerstman/redux-with-code-splitting-and-type-checking-205195aded46).
331-
- Understand that TypeScript does have its trade-offs. It is a good idea to understand when these trade-offs are worth it in your application.
412+
- Redux library documentation:
413+
- [React-Redux docs: Static Typing](https://react-redux.js.org/using-react-redux/static-typing): Examples of how to use the React-Redux APIs with TypeScript
414+
- [Redux Toolkit docs: Advanced Tutorial](https://redux-toolkit.js.org/tutorials/advanced-tutorial): shows how to use RTK and the React-Redux hooks API with TypeScript
415+
- React + Redux + TypeScript guides:
416+
- [React+TypeScript Cheatsheet](https://github.com/typescript-cheatsheets/react-typescript-cheatsheet): a comprehensive guide to using React with TypeScript
417+
- [React + Redux in TypeScript Guide](https://github.com/piotrwitek/react-redux-typescript-guide): extensive information on patterns for using React and Redux with TypeScript
418+
- Other articles:
419+
- [Redux with Code-Splitting and Type Checking](https://www.matthewgerstman.com/tech/redux-code-split-typecheck/)

0 commit comments

Comments
 (0)