-
-
Notifications
You must be signed in to change notification settings - Fork 15.2k
[DRAFT]: Docs for usage with typescript #3201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 34 commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
3430641
Add typescript page under advanced
da479b2
Add introduction
272adb0
Add outline of practical example with codesandbox link
3149950
Add type checking state section
4d04378
Add type checking actions & action creators section
738c57f
Add type checking reducers section
f0ddf58
Add note in outline section
6dec000
Add notes & considerations section
b3e3b73
Update notes & considerations section
d1c16c2
Add react integration point under notes & considerations section
2a88b4f
Remove "I" in all interface naming
ae201cf
Update naming of actions to be more consistent with rest of docs
6c03c7c
Update action creators to use hoisted functions
27f15c0
Update system and chat reducers to use hoisted functions
31d7706
Remove explicit reducer type in root reducer
2edc413
Remove IAppState section
9a367ef
Add note to using union types when necessary for actions
40b176d
Add reasoning and hints on action creators and rootReducer section
8276fb0
Fix spelling & grammar
d70fef8
Prettier formatting
7cd93db
Update reducers to explicitly indicate return type
68ffdf3
Update second statement in type checking state
markerikson 4e20e24
Update type checking root reducer description
markerikson cc60a0c
Reword type checking state description
ed1534f
Add verbose example for typing action creators
2885481
Provide more insight in type checking reducers
d5f632d
Add discussion on tradeoffs in having types declared in a seperate file
4b73d53
Fix wording
9d5aacc
Add section: usage with react-redux
0a9498b
Fix spelling for type checking actions & action creators section
markerikson aca5fdf
Combine DELETE_MESSAGE action in app demo and documentation
f66eca3
Remove verbosity explanatino and add inline comments for action creators
830b736
Rename "react-redux" to "React Redux"
4ce7188
Add section on usage with redux thunk
c46dfb0
Update actions to follow FSA
a216c97
Update implementation of redux thunk's usage with typescript
b793afa
Remove usage of enums
d930250
Update branch from upstream
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,326 @@ | ||
# Usage with TypeScript | ||
|
||
**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) | ||
|
||
TypeScript has the potential to bring the following benefits to a Redux application: | ||
|
||
1. Type safety for reducers, state and action creators | ||
2. Easy refactoring of typed code | ||
3. A superior developer experience in a team environment | ||
|
||
## A Practical Example | ||
|
||
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. | ||
|
||
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. | ||
|
||
## Type Checking State | ||
|
||
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: | ||
|
||
```ts | ||
// src/store/chat/types.ts | ||
|
||
export interface Message { | ||
user: string | ||
message: string | ||
timestamp: number | ||
} | ||
|
||
export interface ChatState { | ||
messages: Message[] | ||
} | ||
``` | ||
|
||
And then do the same for the system reducer's slice of state: | ||
|
||
```ts | ||
// src/store/system/types.ts | ||
|
||
export interface SystemState { | ||
loggedIn: boolean | ||
session: string | ||
userName: string | ||
} | ||
``` | ||
|
||
Note that we are exporting these interfaces to reuse them later in reducers and action creators. | ||
|
||
## Type Checking Actions & Action Creators | ||
|
||
We will be using TypeScript's enums to declare our action constants. [Enums](https://www.typescriptlang.org/docs/handbook/enums.html) allow us to define a set of named constants. 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. | ||
|
||
Chat Action Constants & Shape: | ||
|
||
```ts | ||
// src/store/chat/types.ts | ||
|
||
export enum ChatActions { | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
SendMessage = 'SEND_MESSAGE', | ||
DeleteMessage = 'DELETE_MESSAGE' | ||
} | ||
|
||
interface SendMessageAction { | ||
type: ChatActions.SendMessage | ||
payload: Message | ||
} | ||
|
||
interface DeleteMessageAction { | ||
type: ChatActions.DeleteMessage | ||
timestamp: number | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
export type ChatActionTypes = SendMessageAction | DeleteMessageAction | ||
This conversation was marked as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
Note that we are using TypeScript's Union Type here to express all possible actions. | ||
|
||
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: | ||
|
||
```ts | ||
// src/store/chat/actions.ts | ||
|
||
import { Message, ChatActions } from './types' | ||
|
||
// TypeScript infers that this function is returning SendMessageAction | ||
export function sendMessage(newMessage: Message) { | ||
return { | ||
type: ChatActions.SendMessage, | ||
payload: newMessage | ||
} | ||
} | ||
|
||
// TypeScript infers that this function is returning DeleteMessageAction | ||
export function deleteMessage(timestamp: number) { | ||
return { | ||
type: ChatActions.DeleteMessage, | ||
timestamp | ||
} | ||
} | ||
``` | ||
|
||
System Action Constants & Shape: | ||
|
||
```ts | ||
// src/store/system/types.ts | ||
|
||
export enum SystemActions { | ||
UpdateSession = 'UPDATE_SESSION' | ||
} | ||
|
||
interface UpdateSessionAction { | ||
type: SystemActions.UpdateSession | ||
payload: SystemState | ||
} | ||
|
||
export type SystemActionTypes = UpdateSessionAction | ||
``` | ||
|
||
With these types we can now also type check system's action creators: | ||
|
||
```ts | ||
// src/store/system/actions.ts | ||
|
||
import { SystemActions, SystemState } from './types' | ||
|
||
export function updateSession(newSession: SystemState) { | ||
return { | ||
type: SystemActions.UpdateSession, | ||
payload: newSession | ||
} | ||
} | ||
``` | ||
|
||
## Type Checking Reducers | ||
|
||
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`. | ||
|
||
Type checked chat reducer: | ||
This conversation was marked as resolved.
Show resolved
Hide resolved
|
||
|
||
```ts | ||
// src/store/chat/reducers.ts | ||
|
||
import { ChatState, ChatActions, ChatActionTypes } from './types' | ||
|
||
const initialState: ChatState = { | ||
messages: [] | ||
} | ||
|
||
export function chatReducer( | ||
state = initialState, | ||
action: ChatActionTypes | ||
): ChatState { | ||
switch (action.type) { | ||
This conversation was marked as resolved.
Show resolved
Hide resolved
|
||
case ChatActions.SendMessage: | ||
return { | ||
messages: [...state.messages, action.payload] | ||
} | ||
case ChatActions.DeleteMessage: | ||
return { | ||
messages: state.messages.filter( | ||
message => message.timestamp !== action.timestamp | ||
) | ||
} | ||
default: | ||
return state | ||
} | ||
} | ||
``` | ||
|
||
Type checked system reducer: | ||
|
||
```ts | ||
// src/store/system/reducers.ts | ||
|
||
import { SystemActions, SystemState, SystemActionTypes } from './types' | ||
|
||
const initialState: SystemState = { | ||
loggedIn: false, | ||
session: '', | ||
userName: '' | ||
} | ||
|
||
export function systemReducer( | ||
state = initialState, | ||
action: SystemActionTypes | ||
): SystemState { | ||
switch (action.type) { | ||
case SystemActions.UpdateSession: { | ||
return { | ||
...state, | ||
...action.payload | ||
} | ||
} | ||
default: | ||
return state | ||
} | ||
} | ||
``` | ||
|
||
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`. | ||
|
||
```ts | ||
// src/store/index.ts | ||
|
||
import { systemReducer } from './system/reducers' | ||
import { chatReducer } from './chat/reducers' | ||
|
||
const rootReducer = combineReducers({ | ||
system: systemReducer, | ||
chat: chatReducer | ||
}) | ||
|
||
export type AppState = ReturnType<typeof rootReducer> | ||
``` | ||
|
||
## Usage with React Redux | ||
|
||
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. | ||
|
||
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`. | ||
|
||
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`: | ||
|
||
```ts | ||
// src/App.tsx | ||
|
||
import { AppState } from './store' | ||
|
||
const mapStateToProps = (state: AppState) => ({ | ||
system: state.system, | ||
chat: state.chat | ||
}) | ||
``` | ||
|
||
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: | ||
|
||
```ts | ||
// src/App.tsx | ||
|
||
import { SystemState } from './store/system/types' | ||
|
||
import { ChatState } from './store/chat/types' | ||
|
||
interface AppProps { | ||
chat: ChatState | ||
system: SystemState | ||
} | ||
``` | ||
|
||
We can now use this interface to specify what props the appropriate component will receive like so: | ||
|
||
```ts | ||
// src/App.tsx | ||
|
||
class App extends React.Component<AppProps> { | ||
``` | ||
|
||
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: | ||
|
||
```ts | ||
// src/App.tsx | ||
|
||
import { SystemState } from './store/system/types' | ||
import { updateSession } from './store/system/actions' | ||
|
||
import { ChatState } from './store/chat/types' | ||
import { sendMessage } from './store/chat/actions' | ||
|
||
interface AppProps { | ||
sendMessage: typeof sendMessage | ||
updateSession: typeof updateSession | ||
chat: ChatState | ||
system: SystemState | ||
} | ||
``` | ||
|
||
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. | ||
|
||
## Usage with Redux Thunk | ||
|
||
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`. We will define a type which describes this in a new file: | ||
|
||
```ts | ||
// src/types.ts | ||
|
||
import { Dispatch } from 'redux' | ||
import { AppState } from './store' | ||
|
||
export type ThunkType<T> = (dispatch: Dispatch, getState?: AppState) => T | ||
timdorr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
Note that we are using a TypeScript generic here in order to allow reuse while keeping thunks type safe. Whenever we define a thunk we can use this type like so: | ||
|
||
```ts | ||
// src/thunks.ts | ||
|
||
import { ThunkType } from './types' | ||
import { sendMessage } from './store/chat/actions' | ||
|
||
export function thunkSendMessage(message: string): ThunkType<void> { | ||
return async function(dispatch) { | ||
const asyncResp = await exampleAPI() | ||
|
||
dispatch( | ||
sendMessage({ | ||
message, | ||
user: asyncResp, | ||
timestamp: new Date().getTime() | ||
}) | ||
) | ||
} | ||
} | ||
|
||
function exampleAPI() { | ||
return Promise.resolve('Async Chat Bot') | ||
} | ||
``` | ||
|
||
Thunks can optionally return a value, for this example this thunk does not return any value so we pass `void` for `ThunkType<T>`. It is also highly recommended to use action creators in your dispatch. This is because we have already done work on type checking our actions creators and this work can be reused in our thunks. | ||
|
||
## Notes & Considerations | ||
|
||
- 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. | ||
- There are multiple approaches to type checking redux, this is just one of many approaches. | ||
- 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). | ||
- 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. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.