Skip to content

Commit de1f45a

Browse files
Hersh Varshneitimdorr
authored andcommitted
[DRAFT]: Docs for usage with typescript (#3201)
* Add typescript page under advanced * Add introduction * Add outline of practical example with codesandbox link * Add type checking state section * Add type checking actions & action creators section * Add type checking reducers section * Add note in outline section * Add notes & considerations section * Update notes & considerations section * Add react integration point under notes & considerations section * Remove "I" in all interface naming * Update naming of actions to be more consistent with rest of docs * Update action creators to use hoisted functions * Update system and chat reducers to use hoisted functions * Remove explicit reducer type in root reducer * Remove IAppState section * Add note to using union types when necessary for actions * Add reasoning and hints on action creators and rootReducer section * Fix spelling & grammar * Prettier formatting * Update reducers to explicitly indicate return type * Update second statement in type checking state Co-Authored-By: HershVar <hersh.varshnei@gmail.com> * Update type checking root reducer description Co-Authored-By: HershVar <hersh.varshnei@gmail.com> * Reword type checking state description * Add verbose example for typing action creators * Provide more insight in type checking reducers * Add discussion on tradeoffs in having types declared in a seperate file * Fix wording * Add section: usage with react-redux * Fix spelling for type checking actions & action creators section Co-Authored-By: HershVar <hersh.varshnei@gmail.com> * Combine DELETE_MESSAGE action in app demo and documentation * Remove verbosity explanatino and add inline comments for action creators * Rename "react-redux" to "React Redux" * Add section on usage with redux thunk * Update actions to follow FSA * Update implementation of redux thunk's usage with typescript * Remove usage of enums
1 parent 686d29b commit de1f45a

File tree

1 file changed

+325
-0
lines changed

1 file changed

+325
-0
lines changed
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
# Usage with TypeScript
2+
3+
**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)
4+
5+
TypeScript has the potential to bring the following benefits to a Redux application:
6+
7+
1. Type safety for reducers, state and action creators
8+
2. Easy refactoring of typed code
9+
3. A superior developer experience in a team environment
10+
11+
## A Practical Example
12+
13+
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.
14+
15+
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.
16+
17+
## Type Checking State
18+
19+
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:
20+
21+
```ts
22+
// src/store/chat/types.ts
23+
24+
export interface Message {
25+
user: string
26+
message: string
27+
timestamp: number
28+
}
29+
30+
export interface ChatState {
31+
messages: Message[]
32+
}
33+
```
34+
35+
And then do the same for the system reducer's slice of state:
36+
37+
```ts
38+
// src/store/system/types.ts
39+
40+
export interface SystemState {
41+
loggedIn: boolean
42+
session: string
43+
userName: string
44+
}
45+
```
46+
47+
Note that we are exporting these interfaces to reuse them later in reducers and action creators.
48+
49+
## Type Checking Actions & Action Creators
50+
51+
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.
52+
53+
Chat Action Constants & Shape:
54+
55+
```ts
56+
// src/store/chat/types.ts
57+
export const SEND_MESSAGE = 'SEND_MESSAGE'
58+
export const DELETE_MESSAGE = 'DELETE_MESSAGE'
59+
60+
interface SendMessageAction {
61+
type: typeof SEND_MESSAGE
62+
payload: Message
63+
}
64+
65+
interface DeleteMessageAction {
66+
type: typeof DELETE_MESSAGE
67+
meta: {
68+
timestamp: number
69+
}
70+
}
71+
72+
export type ChatActionTypes = SendMessageAction | DeleteMessageAction
73+
```
74+
75+
Note that we are using TypeScript's Union Type here to express all possible actions.
76+
77+
With these types declared we can now also type check chat's action creators. In this case we are taking advantage of TypeScript's inference:
78+
79+
```ts
80+
// src/store/chat/actions.ts
81+
82+
import { Message, SEND_MESSAGE, DELETE_MESSAGE } from './types'
83+
84+
// TypeScript infers that this function is returning SendMessageAction
85+
export function sendMessage(newMessage: Message) {
86+
return {
87+
type: SEND_MESSAGE,
88+
payload: newMessage
89+
}
90+
}
91+
92+
// TypeScript infers that this function is returning DeleteMessageAction
93+
export function deleteMessage(timestamp: number) {
94+
return {
95+
type: DELETE_MESSAGE,
96+
meta: {
97+
timestamp
98+
}
99+
}
100+
}
101+
```
102+
103+
System Action Constants & Shape:
104+
105+
```ts
106+
// src/store/system/types.ts
107+
export const UPDATE_SESSION = 'UPDATE_SESSION'
108+
109+
interface UpdateSessionAction {
110+
type: typeof UPDATE_SESSION
111+
payload: SystemState
112+
}
113+
114+
export type SystemActionTypes = UpdateSessionAction
115+
```
116+
117+
With these types we can now also type check system's action creators:
118+
119+
```ts
120+
// src/store/system/actions.ts
121+
122+
import { SystemState, UPDATE_SESSION } from './types'
123+
124+
export function updateSession(newSession: SystemState) {
125+
return {
126+
type: UPDATE_SESSION,
127+
payload: newSession
128+
}
129+
}
130+
```
131+
132+
## Type Checking Reducers
133+
134+
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`.
135+
136+
Type checked chat reducer:
137+
138+
```ts
139+
// src/store/chat/reducers.ts
140+
141+
import {
142+
ChatState,
143+
ChatActions,
144+
ChatActionTypes,
145+
SEND_MESSAGE,
146+
DELETE_MESSAGE
147+
} from './types'
148+
149+
const initialState: ChatState = {
150+
messages: []
151+
}
152+
153+
export function chatReducer(
154+
state = initialState,
155+
action: ChatActionTypes
156+
): ChatState {
157+
switch (action.type) {
158+
case SEND_MESSAGE:
159+
return {
160+
messages: [...state.messages, action.payload]
161+
}
162+
case DELETE_MESSAGE:
163+
return {
164+
messages: state.messages.filter(
165+
message => message.timestamp !== action.meta.timestamp
166+
)
167+
}
168+
default:
169+
return state
170+
}
171+
}
172+
```
173+
174+
Type checked system reducer:
175+
176+
```ts
177+
// src/store/system/reducers.ts
178+
179+
import {
180+
SystemActions,
181+
SystemState,
182+
SystemActionTypes,
183+
UPDATE_SESSION
184+
} from './types'
185+
186+
const initialState: SystemState = {
187+
loggedIn: false,
188+
session: '',
189+
userName: ''
190+
}
191+
192+
export function systemReducer(
193+
state = initialState,
194+
action: SystemActionTypes
195+
): SystemState {
196+
switch (action.type) {
197+
case UPDATE_SESSION: {
198+
return {
199+
...state,
200+
...action.payload
201+
}
202+
}
203+
default:
204+
return state
205+
}
206+
}
207+
```
208+
209+
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`.
210+
211+
```ts
212+
// src/store/index.ts
213+
214+
import { systemReducer } from './system/reducers'
215+
import { chatReducer } from './chat/reducers'
216+
217+
const rootReducer = combineReducers({
218+
system: systemReducer,
219+
chat: chatReducer
220+
})
221+
222+
export type AppState = ReturnType<typeof rootReducer>
223+
```
224+
225+
## Usage with React Redux
226+
227+
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.
228+
229+
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`.
230+
231+
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`:
232+
233+
```ts
234+
// src/App.tsx
235+
236+
import { AppState } from './store'
237+
238+
const mapStateToProps = (state: AppState) => ({
239+
system: state.system,
240+
chat: state.chat
241+
})
242+
```
243+
244+
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:
245+
246+
```ts
247+
// src/App.tsx
248+
249+
import { SystemState } from './store/system/types'
250+
251+
import { ChatState } from './store/chat/types'
252+
253+
interface AppProps {
254+
chat: ChatState
255+
system: SystemState
256+
}
257+
```
258+
259+
We can now use this interface to specify what props the appropriate component will receive like so:
260+
261+
```ts
262+
// src/App.tsx
263+
264+
class App extends React.Component<AppProps> {
265+
```
266+
267+
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:
268+
269+
```ts
270+
// src/App.tsx
271+
272+
import { SystemState } from './store/system/types'
273+
import { updateSession } from './store/system/actions'
274+
275+
import { ChatState } from './store/chat/types'
276+
import { sendMessage } from './store/chat/actions'
277+
278+
interface AppProps {
279+
sendMessage: typeof sendMessage
280+
updateSession: typeof updateSession
281+
chat: ChatState
282+
system: SystemState
283+
}
284+
```
285+
286+
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.
287+
288+
## Usage with Redux Thunk
289+
290+
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:
291+
292+
```ts
293+
// src/thunks.ts
294+
295+
import { Action } from 'redux'
296+
import { sendMessage } from './store/chat/actions'
297+
import { AppState } from './store'
298+
import { ThunkAction } from 'redux-thunk'
299+
300+
export const thunkSendMessage = (
301+
message: string
302+
): ThunkAction<void, AppState, null, Action<string>> => async dispatch => {
303+
const asyncResp = await exampleAPI()
304+
dispatch(
305+
sendMessage({
306+
message,
307+
user: asyncResp,
308+
timestamp: new Date().getTime()
309+
})
310+
)
311+
}
312+
313+
function exampleAPI() {
314+
return Promise.resolve('Async Chat Bot')
315+
}
316+
```
317+
318+
It is highly recommended to use action creators in your dispatch since we can reuse the work that has been to type check these functions.
319+
320+
## Notes & Considerations
321+
322+
- 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.
323+
- There are multiple approaches to type checking redux, this is just one of many approaches.
324+
- 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).
325+
- 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.

0 commit comments

Comments
 (0)