1 | <div align="center">
|
2 |
|
3 | # typesafe-actions
|
4 |
|
5 | Typesafe utilities designed to reduce types **verbosity**
|
6 | and **complexity** in Redux Architecture.
|
7 |
|
8 | _This library is part of the [React & Redux TypeScript Guide](https://github.com/piotrwitek/react-redux-typescript-guide)_ ecosystem :book:
|
9 |
|
10 | [![Latest Stable Version](https://img.shields.io/npm/v/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
|
11 | [![NPM Downloads](https://img.shields.io/npm/dm/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
|
12 | [![NPM Downloads](https://img.shields.io/npm/dt/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
|
13 | [![Bundlephobia Size](https://img.shields.io/bundlephobia/minzip/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
|
14 |
|
15 | [![Build Status](https://semaphoreci.com/api/v1/piotrekwitek/typesafe-actions/branches/master/shields_badge.svg)](https://semaphoreci.com/piotrekwitek/typesafe-actions)
|
16 | [![Dependency Status](https://img.shields.io/david/piotrwitek/typesafe-actions.svg)](https://david-dm.org/piotrwitek/typesafe-actions)
|
17 | [![License](https://img.shields.io/npm/l/typesafe-actions.svg?style=flat)](https://david-dm.org/piotrwitek/typesafe-actions?type=peer)
|
18 | [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/typesafe-actions)
|
19 |
|
20 | _Found it useful? Want more updates?_
|
21 |
|
22 | [**Show your support by giving a :star:**](https://github.com/piotrwitek/typesafe-actions/stargazers)
|
23 |
|
24 |
|
25 |
|
26 | <a href="https://www.buymeacoffee.com/piotrekwitek">
|
27 | <img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me a Coffee">
|
28 | </a>
|
29 |
|
30 |
|
31 |
|
32 | <a href="https://www.patreon.com/piotrekwitek">
|
33 | <img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron" width="160">
|
34 | </a>
|
35 |
|
36 | <br/><hr/>
|
37 |
|
38 | ## What's new?
|
39 |
|
40 | :tada: _Now updated to support **TypeScript v3.7**_ :tada:
|
41 |
|
42 | :warning: Library was recently updated to v5 :warning:
|
43 | <br/>*Current API Docs and Tutorial are outdated (from v4), so temporarily please use this issue as [v5.x.x API Docs](https://github.com/piotrwitek/typesafe-actions/issues/143).*
|
44 |
|
45 | <hr/><br/>
|
46 |
|
47 | </div>
|
48 |
|
49 | ### **Features**
|
50 | - Easily create completely typesafe [Actions](#action-creators-api) or even [Async Actions](#createasyncaction)
|
51 | - No boilerplate and completely typesafe [Reducers](#reducer-creators-api)
|
52 | - Game-changing [Helper Types](#type-helpers-api) for Redux
|
53 |
|
54 | ### **Playgrounds & Examples**
|
55 |
|
56 | - Todo-App playground: [Codesandbox](https://codesandbox.io/s/github/piotrwitek/typesafe-actions/tree/master/codesandbox)
|
57 | - React, Redux, TypeScript - RealWorld App: [Github](https://github.com/piotrwitek/react-redux-typescript-realworld-app) | [Demo](https://react-redux-typescript-realworld-app.netlify.com/)
|
58 |
|
59 | ### **Goals**
|
60 |
|
61 | - **Secure and Minimal** - no third-party dependencies, according to `size-snapshot` (Minified: 3.48 KB, Gzipped: 1.03 KB), check also on [bundlephobia](https://bundlephobia.com/result?p=typesafe-actions)
|
62 | - **Optimized** - distribution packages bundled in 3 different formats (`cjs`, `esm` and `umd`) with separate bundles for dev & prod (same as `react`)
|
63 | - **Quality** - complete test-suite for an entire API surface containing regular runtime tests and extra type-tests to guarantee **type soundness** and to prevent regressions in the future TypeScript versions
|
64 | - **Performance** - integrated performance benchmarks to guarantee that the computational complexity of types are in check and there are no slow-downs when your application grow `npm run benchmark:XXX`
|
65 |
|
66 | ---
|
67 |
|
68 | ## Table of Contents
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | - [Installation](#installation)
|
75 | - [Tutorial v4 (v5 is WIP #188)](#tutorial-v4-v5-is-wip-188)
|
76 | - [Constants](#constants)
|
77 | - [Actions](#actions)
|
78 | - [1. Basic actions](#1-basic-actions)
|
79 | - [2. FSA compliant actions](#2-fsa-compliant-actions)
|
80 | - [3. Custom actions (non-standard use-cases)](#3-custom-actions-non-standard-use-cases)
|
81 | - [Action Helpers](#action-helpers)
|
82 | - [Using action-creators instances instead of type-constants](#using-action-creators-instances-instead-of-type-constants)
|
83 | - [Using regular type-constants](#using-regular-type-constants)
|
84 | - [Reducers](#reducers)
|
85 | - [Extending internal types to enable type-free syntax with `createReducer`](#extending-internal-types-to-enable-type-free-syntax-with-createreducer)
|
86 | - [Using createReducer API with type-free syntax](#using-createreducer-api-with-type-free-syntax)
|
87 | - [Alternative usage with regular switch reducer](#alternative-usage-with-regular-switch-reducer)
|
88 | - [Async-Flows](#async-flows)
|
89 | - [With `redux-observable` epics](#with-redux-observable-epics)
|
90 | - [With `redux-saga` sagas](#with-redux-saga-sagas)
|
91 | - [API Docs v4 (v5 is WIP #189)](#api-docs-v4-v5-is-wip-189)
|
92 | - [Action-Creators API](#action-creators-api)
|
93 | - [`action`](#action)
|
94 | - [`createAction`](#createaction)
|
95 | - [`createStandardAction`](#createstandardaction)
|
96 | - [`createCustomAction`](#createcustomaction)
|
97 | - [`createAsyncAction`](#createasyncaction)
|
98 | - [Reducer-Creators API](#reducer-creators-api)
|
99 | - [`createReducer`](#createreducer)
|
100 | - [Action-Helpers API](#action-helpers-api)
|
101 | - [`getType`](#gettype)
|
102 | - [`isActionOf`](#isactionof)
|
103 | - [`isOfType`](#isoftype)
|
104 | - [Type-Helpers API](#type-helpers-api)
|
105 | - [`ActionType`](#actiontype)
|
106 | - [`StateType`](#statetype)
|
107 | - [Migration Guides](#migration-guides)
|
108 | - [`v4.x.x` to `v5.x.x`](#v4xx-to-v5xx)
|
109 | - [`v3.x.x` to `v4.x.x`](#v3xx-to-v4xx)
|
110 | - [`v2.x.x` to `v3.x.x`](#v2xx-to-v3xx)
|
111 | - [`v1.x.x` to `v2.x.x`](#v1xx-to-v2xx)
|
112 | - [Migrating from `redux-actions` to `typesafe-actions`](#migrating-from-redux-actions-to-typesafe-actions)
|
113 | - [Compatibility Notes](#compatibility-notes)
|
114 | - [Recipes](#recipes)
|
115 | - [Restrict Meta type in `action` creator](#restrict-meta-type-in-action-creator)
|
116 | - [Compare to others](#compare-to-others)
|
117 | - [`redux-actions`](#redux-actions)
|
118 | - [Motivation](#motivation)
|
119 | - [Contributing](#contributing)
|
120 | - [Funding Issues](#funding-issues)
|
121 | - [License](#license)
|
122 |
|
123 |
|
124 |
|
125 | <hr/>
|
126 |
|
127 | ## Installation
|
128 |
|
129 | ```bash
|
130 | # NPM
|
131 | npm install typesafe-actions
|
132 |
|
133 | # YARN
|
134 | yarn add typesafe-actions
|
135 | ```
|
136 |
|
137 | [⇧ back to top](#table-of-contents)
|
138 |
|
139 | ---
|
140 |
|
141 | ## Tutorial v4 (v5 is WIP [#188](https://github.com/piotrwitek/typesafe-actions/issues/188))
|
142 |
|
143 | To showcase the flexibility and the power of the **type-safety** provided by this library, let's build the most common parts of a typical todo-app using a Redux architecture:
|
144 |
|
145 | > **WARNING**
|
146 | > Please make sure that you are familiar with the following concepts of programming languages to be able to follow along: [Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html), [Control flow analysis](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#control-flow-based-type-analysis), [Tagged union types](https://github.com/Microsoft/TypeScript/wiki/What%27s-new-in-TypeScript#tagged-union-types), [Generics](https://www.typescriptlang.org/docs/handbook/generics.html) and [Advanced Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html).
|
147 |
|
148 | [⇧ back to top](#table-of-contents)
|
149 |
|
150 | ### Constants
|
151 |
|
152 | > **RECOMMENDATION:**
|
153 | > When using `typesafe-actions` in your project you won't need to export and reuse **string constants**. It's because **action-creators** created by this library have static property with **action type** that you can easily access using **actions-helpers** and then use it in reducers, epics, sagas, and basically any other place. This will simplify your codebase and remove some boilerplate code associated with the usage of **string constants**. Check our `/codesandbox` application to learn some best-practices to create such codebase.
|
154 |
|
155 | **Limitations of TypeScript when working with string constants** - when using **string constants** as action `type` property, please make sure to use **simple string literal assignment with const**. This limitation is coming from the type-system, because all the **dynamic string operations** (e.g. string concatenation, template strings and also object used as a map) will widen the literal type to its super-type, `string`. As a result this will break contextual typing for **action** object in reducer cases.
|
156 |
|
157 | ```ts
|
158 | // Example file: './constants.ts'
|
159 |
|
160 | // WARNING: Incorrect usage
|
161 | export const ADD = prefix + 'ADD'; // => string
|
162 | export const ADD = `${prefix}/ADD`; // => string
|
163 | export default {
|
164 | ADD: '@prefix/ADD', // => string
|
165 | }
|
166 |
|
167 | // Correct usage
|
168 | export const ADD = '@prefix/ADD'; // => '@prefix/ADD'
|
169 | export const TOGGLE = '@prefix/TOGGLE'; // => '@prefix/TOGGLE'
|
170 | export default ({
|
171 | ADD: '@prefix/ADD', // => '@prefix/ADD'
|
172 | } as const) // working in TS v3.4 and above => https://github.com/Microsoft/TypeScript/pull/29510
|
173 | ```
|
174 |
|
175 | [⇧ back to top](#table-of-contents)
|
176 |
|
177 | ### Actions
|
178 |
|
179 | Different projects have different needs, and conventions vary across teams, and this is why `typesafe-actions` was designed with flexibility in mind. It provides three different major styles so you can choose whichever would be the best fit for your team.
|
180 |
|
181 | #### 1. Basic actions
|
182 | `action` and `createAction` are creators that can create **actions** with predefined properties ({ type, payload, meta }). This make them concise but also opinionated.
|
183 |
|
184 | Important property is that resulting **action-creator** will have a variadic number of arguments and preserve their semantic names `(id, title, amount, etc...)`.
|
185 |
|
186 | This two creators are very similar and the only real difference is that `action` **WILL NOT WORK** with **action-helpers**.
|
187 |
|
188 | ```ts
|
189 | import { action, createAction } from 'typesafe-actions';
|
190 |
|
191 | export const add = (title: string) => action('todos/ADD', { id: cuid(), title, completed: false });
|
192 | // add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }
|
193 |
|
194 | export const add = createAction('todos/ADD', action => {
|
195 | // Note: "action" callback does not need "type" parameter
|
196 | return (title: string) => action({ id: cuid(), title, completed: false });
|
197 | });
|
198 | // add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }
|
199 | ```
|
200 |
|
201 | #### 2. FSA compliant actions
|
202 | This style is aligned with [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action), so your **action** object shape is constrained to `({ type, payload, meta, error })`. It is using **generic type arguments** for `meta` and `payload` to simplify creation of type-safe action-creators.
|
203 |
|
204 | It is important to notice that in the resulting **action-creator** arguments are also constrained to the predefined: `(payload, meta)`, making it the most opinionated creator.
|
205 |
|
206 | > **TIP**: This creator is the most compatible with `redux-actions` in case you are migrating.
|
207 |
|
208 | ```ts
|
209 | import { createStandardAction } from 'typesafe-actions';
|
210 |
|
211 | export const toggle = createStandardAction('todos/TOGGLE')<string>();
|
212 | // toggle: (payload: string) => { type: "todos/TOGGLE"; payload: string; }
|
213 |
|
214 | export const add = createStandardAction('todos/ADD').map(
|
215 | (title: string) => ({
|
216 | payload: { id: cuid(), title, completed: false },
|
217 | })
|
218 | );
|
219 | // add: (payload: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }
|
220 | ```
|
221 |
|
222 | #### 3. Custom actions (non-standard use-cases)
|
223 |
|
224 | This approach will give us the most flexibility of all creators, providing a variadic number of named parameters and custom properties on **action** object to fit all the custom use-cases.
|
225 |
|
226 | ```ts
|
227 | import { createCustomAction } from 'typesafe-actions';
|
228 |
|
229 | const add = createCustomAction('todos/ADD', type => {
|
230 | return (title: string) => ({ type, id: cuid(), title, completed: false });
|
231 | });
|
232 | // add: (title: string) => { type: "todos/ADD"; id: string; title: string; completed: boolean; }
|
233 | ```
|
234 |
|
235 | > **TIP**: For more examples please check the [API Docs](#table-of-contents).
|
236 |
|
237 | > **RECOMMENDATION**
|
238 | > Common approach is to create a `RootAction` in the central point of your redux store - it will represent all possible action types in your application. You can even merge it with third-party action types as shown below to make your model complete.
|
239 |
|
240 | ```ts
|
241 | // types.d.ts
|
242 | // example of including `react-router` actions in `RootAction`
|
243 | import { RouterAction, LocationChangeAction } from 'react-router-redux';
|
244 | import { TodosAction } from '../features/todos';
|
245 |
|
246 | type ReactRouterAction = RouterAction | LocationChangeAction;
|
247 |
|
248 | export type RootAction =
|
249 | | ReactRouterAction
|
250 | | TodosAction;
|
251 | ```
|
252 |
|
253 | [⇧ back to top](#table-of-contents)
|
254 |
|
255 | ### Action Helpers
|
256 |
|
257 | Now I want to show you **action-helpers** and explain their use-cases. We're going to implement a side-effect responsible for showing a success toast when user adds a new todo.
|
258 |
|
259 | Important thing to notice is that all these helpers are acting as a **type-guard** so they'll narrow **tagged union type** (`RootAction`) to a specific action type that we want.
|
260 |
|
261 | #### Using action-creators instances instead of type-constants
|
262 |
|
263 | Instead of **type-constants** we can use **action-creators** instance to match specific actions in reducers and epics cases. It works by adding a static property on **action-creator** instance which contains the `type` string.
|
264 |
|
265 | The most common one is `getType`, which is useful for regular reducer switch cases:
|
266 |
|
267 | ```ts
|
268 | switch (action.type) {
|
269 | case getType(todos.add):
|
270 | // below action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
|
271 | return [...state, action.payload];
|
272 | ...
|
273 | ```
|
274 |
|
275 | Then we have the `isActionOf` helper which accept **action-creator** as first parameter matching actions with corresponding type passed as second parameter (it's a curried function).
|
276 |
|
277 | ```ts
|
278 | // epics.ts
|
279 | import { isActionOf } from 'typesafe-actions';
|
280 |
|
281 | import { add } from './actions';
|
282 |
|
283 | const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { toastService }) =>
|
284 | action$.pipe(
|
285 | filter(isActionOf(add)),
|
286 | tap(action => { // here action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
|
287 | toastService.success(...);
|
288 | })
|
289 | ...
|
290 |
|
291 | // Works with multiple actions! (with type-safety up to 5)
|
292 | action$.pipe(
|
293 | filter(isActionOf([add, toggle])) // here action type is narrowed to a smaller union:
|
294 | // { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
|
295 | ```
|
296 |
|
297 | #### Using regular type-constants
|
298 | Alternatively if your team prefers to use regular **type-constants** you can still do that.
|
299 |
|
300 | We have an equivalent helper (`isOfType`) which accept **type-constants** as parameter providing the same functionality.
|
301 |
|
302 | ```ts
|
303 | // epics.ts
|
304 | import { isOfType } from 'typesafe-actions';
|
305 |
|
306 | import { ADD } from './constants';
|
307 |
|
308 | const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { toastService }) =>
|
309 | action$.pipe(
|
310 | filter(isOfType(ADD)),
|
311 | tap(action => { // here action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
|
312 | ...
|
313 |
|
314 | // Works with multiple actions! (with type-safety up to 5)
|
315 | action$.pipe(
|
316 | filter(isOfType([ADD, TOGGLE])) // here action type is narrowed to a smaller union:
|
317 | // { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
|
318 | ```
|
319 |
|
320 | > **TIP:** you can use action-helpers with other types of conditional statements.
|
321 |
|
322 | ```ts
|
323 | import { isActionOf, isOfType } from 'typesafe-actions';
|
324 |
|
325 | if (isActionOf(actions.add, action)) {
|
326 | // here action is narrowed to: { type: "todos/ADD"; payload: Todo; }
|
327 | }
|
328 | // or with type constants
|
329 | if (isOfType(types.ADD, action)) {
|
330 | // here action is narrowed to: { type: "todos/ADD"; payload: Todo; }
|
331 | }
|
332 | ```
|
333 |
|
334 | [⇧ back to top](#table-of-contents)
|
335 |
|
336 | ### Reducers
|
337 |
|
338 | #### Extending internal types to enable type-free syntax with `createReducer`
|
339 |
|
340 | We can extend internal types of `typesafe-actions` module with `RootAction` definition of our application so that you don't need to pass generic type arguments with `createReducer` API:
|
341 |
|
342 | ```ts
|
343 | // types.d.ts
|
344 | import { StateType, ActionType } from 'typesafe-actions';
|
345 |
|
346 | export type RootAction = ActionType<typeof import('./actions').default>;
|
347 |
|
348 | declare module 'typesafe-actions' {
|
349 | interface Types {
|
350 | RootAction: RootAction;
|
351 | }
|
352 | }
|
353 |
|
354 | // now you can use
|
355 | createReducer(...)
|
356 | // instead of
|
357 | createReducer<State, Action>(...)
|
358 | ```
|
359 |
|
360 | #### Using createReducer API with type-free syntax
|
361 |
|
362 | We can prevent a lot of boilerplate code and type errors using this powerfull and completely typesafe API.
|
363 |
|
364 | Using handleAction chain API:
|
365 | ```ts
|
366 | // using action-creators
|
367 | const counterReducer = createReducer(0)
|
368 | // state and action type is automatically inferred and return type is validated to be exact type
|
369 | .handleAction(add, (state, action) => state + action.payload)
|
370 | .handleAction(add, ... // <= error is shown on duplicated or invalid actions
|
371 | .handleAction(increment, (state, _) => state + 1)
|
372 | .handleAction(... // <= error is shown when all actions are handled
|
373 |
|
374 | // or handle multiple actions using array
|
375 | .handleAction([add, increment], (state, action) =>
|
376 | state + (action.type === 'ADD' ? action.payload : 1)
|
377 | );
|
378 |
|
379 | // all the same scenarios are working when using type-constants
|
380 | const counterReducer = createReducer(0)
|
381 | .handleAction('ADD', (state, action) => state + action.payload)
|
382 | .handleAction('INCREMENT', (state, _) => state + 1);
|
383 |
|
384 | counterReducer(0, add(4)); // => 4
|
385 | counterReducer(0, increment()); // => 1
|
386 | ```
|
387 |
|
388 | #### Alternative usage with regular switch reducer
|
389 |
|
390 | First we need to start by generating a **tagged union type** of actions (`TodosAction`). It's very easy to do by using `ActionType` **type-helper**.
|
391 |
|
392 | ```ts
|
393 | import { ActionType } from 'typesafe-actions';
|
394 |
|
395 | import * as todos from './actions';
|
396 | export type TodosAction = ActionType<typeof todos>;
|
397 | ```
|
398 |
|
399 | Now we define a regular reducer function by annotating `state` and `action` arguments with their respective types (`TodosAction` for action type).
|
400 |
|
401 | ```ts
|
402 | export default (state: Todo[] = [], action: TodosAction) => {
|
403 | ```
|
404 |
|
405 | Now in the switch cases we can use the `type` property of action to narrowing the union type of `TodosAction` to an action that is corresponding to that type.
|
406 |
|
407 | ```ts
|
408 | switch (action.type) {
|
409 | case getType(add):
|
410 | // below action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
|
411 | return [...state, action.payload];
|
412 | ...
|
413 | ```
|
414 |
|
415 | [⇧ back to top](#table-of-contents)
|
416 |
|
417 | ### Async-Flows
|
418 |
|
419 | #### With `redux-observable` epics
|
420 |
|
421 | To handle an async-flow of http request lets implement an `epic`. The `epic` will call a remote API using an injected `todosApi` client, which will return a Promise that we'll need to handle by using three different actions that correspond to triggering, success and failure.
|
422 |
|
423 | To help us simplify the creation process of necessary action-creators, we'll use `createAsyncAction` function providing us with a nice common interface object `{ request: ... , success: ... , failure: ... }` that will nicely fit with the functional API of `RxJS`.
|
424 | This will mitigate **redux verbosity** and greatly reduce the maintenance cost of type annotations for **actions** and **action-creators** that would otherwise be written explicitly.
|
425 |
|
426 | ```ts
|
427 | // actions.ts
|
428 | import { createAsyncAction } from 'typesafe-actions';
|
429 |
|
430 | const fetchTodosAsync = createAsyncAction(
|
431 | 'FETCH_TODOS_REQUEST',
|
432 | 'FETCH_TODOS_SUCCESS',
|
433 | 'FETCH_TODOS_FAILURE',
|
434 | 'FETCH_TODOS_CANCEL'
|
435 | )<string, Todo[], Error, string>();
|
436 |
|
437 | // epics.ts
|
438 | import { fetchTodosAsync } from './actions';
|
439 |
|
440 | const fetchTodosFlow: Epic<RootAction, RootAction, RootState, Services> = (action$, state$, { todosApi }) =>
|
441 | action$.pipe(
|
442 | filter(isActionOf(fetchTodosAsync.request)),
|
443 | switchMap(action =>
|
444 | from(todosApi.getAll(action.payload)).pipe(
|
445 | map(fetchTodosAsync.success),
|
446 | catchError((message: string) => of(fetchTodosAsync.failure(message))),
|
447 | takeUntil(action$.pipe(filter(isActionOf(fetchTodosAsync.cancel)))),
|
448 | )
|
449 | );
|
450 | ```
|
451 |
|
452 | #### With `redux-saga` sagas
|
453 | With sagas it's not possible to achieve the same degree of type-safety as with epics because of limitations coming from `redux-saga` API design.
|
454 |
|
455 | Typescript issues:
|
456 | - [Typescript does not currently infer types resulting from a `yield` statement](https://github.com/Microsoft/TypeScript/issues/2983) so you have to manually assert the type e.g. `const response: Todo[] = yield call(...`
|
457 |
|
458 | *Here is the latest recommendation although it's not fully optimal. If you managed to cook something better, please open an issue to share your finding with us.*
|
459 |
|
460 | ```ts
|
461 | import { createAsyncAction, createReducer } from 'typesafe-actions';
|
462 | import { put, call, takeEvery } from 'redux-saga/effetcs';
|
463 |
|
464 | // Create the set of async actions
|
465 | const fetchTodosAsync = createAsyncAction(
|
466 | 'FETCH_TODOS_REQUEST',
|
467 | 'FETCH_TODOS_SUCCESS',
|
468 | 'FETCH_TODOS_FAILURE'
|
469 | )<string, Todo[], Error>();
|
470 |
|
471 | // Handle request saga
|
472 | function* addTodoSaga(action: ReturnType<typeof fetchTodosAsync.request>): Generator {
|
473 | try {
|
474 | const response: Todo[] = yield call(todosApi.getAll, action.payload);
|
475 |
|
476 | yield put(fetchTodosAsync.success(response));
|
477 | } catch (err) {
|
478 | yield put(fetchTodosAsync.failure(err));
|
479 | }
|
480 | }
|
481 |
|
482 | // Main saga
|
483 | function* mainSaga() {
|
484 | yield all([
|
485 | takeEvery(fetchTodosAsync.request, addTodoSaga),
|
486 | ]);
|
487 | }
|
488 |
|
489 | // Handle success reducer
|
490 | export const todoReducer = createReducer({})
|
491 | .handleAction(fetchTodosAsync.success, (state, action) => ({ ...state, todos: action.payload }));
|
492 | ```
|
493 |
|
494 | [⇧ back to top](#table-of-contents)
|
495 |
|
496 | ---
|
497 |
|
498 | ## API Docs v4 (v5 is WIP [#189](https://github.com/piotrwitek/typesafe-actions/issues/189))
|
499 |
|
500 | ### Action-Creators API
|
501 |
|
502 | #### `action`
|
503 |
|
504 | _Simple **action factory function** to simplify creation of type-safe actions._
|
505 |
|
506 | > **WARNING**:
|
507 | > This approach will **NOT WORK** with **action-helpers** (such as `getType` and `isActionOf`) because it is creating **action objects** while all the other creator functions are returning **enhanced action-creators**.
|
508 |
|
509 | ```ts
|
510 | action(type, payload?, meta?, error?)
|
511 | ```
|
512 |
|
513 | Examples:
|
514 | [> Advanced Usage Examples](src/action.spec.ts)
|
515 |
|
516 | ```ts
|
517 | const increment = () => action('INCREMENT');
|
518 | // { type: 'INCREMENT'; }
|
519 |
|
520 | const createUser = (id: number, name: string) =>
|
521 | action('CREATE_USER', { id, name });
|
522 | // { type: 'CREATE_USER'; payload: { id: number; name: string }; }
|
523 |
|
524 | const getUsers = (params?: string) =>
|
525 | action('GET_USERS', undefined, params);
|
526 | // { type: 'GET_USERS'; meta: string | undefined; }
|
527 | ```
|
528 |
|
529 | > **TIP**: Starting from TypeScript v3.4 you can achieve similar results using new `as const` operator.
|
530 |
|
531 | ```ts
|
532 | const increment = () => ({ type: 'INCREMENT' } as const);
|
533 | ```
|
534 |
|
535 | #### `createAction`
|
536 |
|
537 | _Create an enhanced action-creator with unlimited number of arguments._
|
538 | - Resulting action-creator will preserve semantic names of their arguments `(id, title, amount, etc...)`.
|
539 | - Returned action object have predefined properties `({ type, payload, meta })`
|
540 |
|
541 | ```ts
|
542 | createAction(type)
|
543 | createAction(type, actionCallback => {
|
544 | return (namedArg1, namedArg2, ...namedArgN) => actionCallback(payload?, meta?)
|
545 | })
|
546 | ```
|
547 | > **TIP**: Injected `actionCallback` argument is similar to `action` API but doesn't need the "type" parameter
|
548 |
|
549 | Examples:
|
550 | [> Advanced Usage Examples](src/create-action.spec.ts)
|
551 |
|
552 | ```ts
|
553 | import { createAction } from 'typesafe-actions';
|
554 |
|
555 | // - with type only
|
556 | const increment = createAction('INCREMENT');
|
557 | dispatch(increment());
|
558 | // { type: 'INCREMENT' };
|
559 |
|
560 | // - with type and payload
|
561 | const add = createAction('ADD', action => {
|
562 | return (amount: number) => action(amount);
|
563 | });
|
564 | dispatch(add(10));
|
565 | // { type: 'ADD', payload: number }
|
566 |
|
567 | // - with type and meta
|
568 | const getTodos = createAction('GET_TODOS', action => {
|
569 | return (params: Params) => action(undefined, params);
|
570 | });
|
571 | dispatch(getTodos('some_meta'));
|
572 | // { type: 'GET_TODOS', meta: Params }
|
573 |
|
574 | // - and finally with type, payload and meta
|
575 | const getTodo = createAction('GET_TODO', action => {
|
576 | return (id: string, meta: string) => action(id, meta);
|
577 | });
|
578 | dispatch(getTodo('some_id', 'some_meta'));
|
579 | // { type: 'GET_TODO', payload: string, meta: string }
|
580 | ```
|
581 |
|
582 | [⇧ back to top](#table-of-contents)
|
583 |
|
584 | #### `createStandardAction`
|
585 |
|
586 | _Create an enhanced action-creator compatible with [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action) to reduce boilerplate and enforce convention._
|
587 | - Resulting action-creator have predefined arguments `(payload, meta)`
|
588 | - Returned action object have predefined properties `({ type, payload, meta, error })`
|
589 | - But it also contains a `.map()` method that allow to map `(payload, meta)` arguments to a custom action object `({ customProp1, customProp2, ...customPropN })`
|
590 |
|
591 | ```ts
|
592 | createStandardAction(type)()
|
593 | createStandardAction(type)<TPayload, TMeta?>()
|
594 | createStandardAction(type).map((payload, meta) => ({ customProp1, customProp2, ...customPropN }))
|
595 | ```
|
596 |
|
597 | > **TIP**: Using `undefined` as generic type parameter you can make the action-creator function require NO parameters.
|
598 |
|
599 | Examples:
|
600 | [> Advanced Usage Examples](src/create-standard-action.spec.ts)
|
601 |
|
602 | ```ts
|
603 | import { createStandardAction } from 'typesafe-actions';
|
604 |
|
605 | // Very concise with use of generic type arguments
|
606 | // - with type only
|
607 | const increment = createStandardAction('INCREMENT')();
|
608 | const increment = createStandardAction('INCREMENT')<undefined>();
|
609 | increment(); // { type: 'INCREMENT' } (no parameters are required)
|
610 |
|
611 |
|
612 | // - with type and payload
|
613 | const add = createStandardAction('ADD')<number>();
|
614 | add(10); // { type: 'ADD', payload: number }
|
615 |
|
616 | // - with type and meta
|
617 | const getData = createStandardAction('GET_DATA')<undefined, string>();
|
618 | getData(undefined, 'meta'); // { type: 'GET_DATA', meta: string }
|
619 |
|
620 | // - and finally with type, payload and meta
|
621 | const getData = createStandardAction('GET_DATA')<number, string>();
|
622 | getData(1, 'meta'); // { type: 'GET_DATA', payload: number, meta: string }
|
623 |
|
624 | // Can map payload and meta arguments to a custom action object
|
625 | const notify = createStandardAction('NOTIFY').map(
|
626 | (payload: string, meta: Meta) => ({
|
627 | from: meta.username,
|
628 | message: `${username}: ${payload}`,
|
629 | messageType: meta.type,
|
630 | datetime: new Date(),
|
631 | })
|
632 | );
|
633 |
|
634 | dispatch(notify('Hello!', { username: 'Piotr', type: 'announcement' }));
|
635 | // { type: 'NOTIFY', from: string, message: string, messageType: MessageType, datetime: Date }
|
636 | ```
|
637 |
|
638 | [⇧ back to top](#table-of-contents)
|
639 |
|
640 | #### `createCustomAction`
|
641 |
|
642 | _Create an enhanced action-creator with unlimited number of arguments and custom properties on action object._
|
643 | - Resulting action-creator will preserve semantic names of their arguments `(id, title, amount, etc...)`.
|
644 | - Returned action object have custom properties `({ type, customProp1, customProp2, ...customPropN })`
|
645 |
|
646 | ```ts
|
647 | createCustomAction(type, type => {
|
648 | return (namedArg1, namedArg2, ...namedArgN) => ({ type, customProp1, customProp2, ...customPropN })
|
649 | })
|
650 | ```
|
651 |
|
652 | Examples:
|
653 | [> Advanced Usage Examples](src/create-action-with-type.spec.ts)
|
654 |
|
655 | ```ts
|
656 | import { createCustomAction } from 'typesafe-actions';
|
657 |
|
658 | const add = createCustomAction('CUSTOM', type => {
|
659 | return (first: number, second: number) => ({ type, customProp1: first, customProp2: second });
|
660 | });
|
661 |
|
662 | dispatch(add(1));
|
663 | // { type: "CUSTOM"; customProp1: number; customProp2: number; }
|
664 | ```
|
665 |
|
666 | [⇧ back to top](#table-of-contents)
|
667 |
|
668 | #### `createAsyncAction`
|
669 |
|
670 | _Create an object containing three enhanced action-creators to simplify handling of async flows (e.g. network request - request/success/failure)._
|
671 |
|
672 | ```ts
|
673 | createAsyncAction(
|
674 | requestType, successType, failureType, cancelType?
|
675 | )<TRequestPayload, TSuccessPayload, TFailurePayload, TCancelPayload?>()
|
676 | ```
|
677 |
|
678 | ##### `AsyncActionCreator`
|
679 |
|
680 | ```ts
|
681 | type AsyncActionCreator<
|
682 | [TRequestType, TRequestPayload],
|
683 | [TSuccessType, TSuccessPayload],
|
684 | [TFailureType, TFailurePayload],
|
685 | [TCancelType, TCancelPayload]?
|
686 | > = {
|
687 | request: StandardActionCreator<TRequestType, TRequestPayload>,
|
688 | success: StandardActionCreator<TSuccessType, TSuccessPayload>,
|
689 | failure: StandardActionCreator<TFailureType, TFailurePayload>,
|
690 | cancel?: StandardActionCreator<TCancelType, TCancelPayload>,
|
691 | }
|
692 | ```
|
693 |
|
694 | > **TIP**: Using `undefined` as generic type parameter you can make the action-creator function require NO parameters.
|
695 |
|
696 | Examples:
|
697 | [> Advanced Usage Examples](src/create-async-action.spec.ts)
|
698 |
|
699 | ```ts
|
700 | import { createAsyncAction, AsyncActionCreator } from 'typesafe-actions';
|
701 |
|
702 | const fetchUsersAsync = createAsyncAction(
|
703 | 'FETCH_USERS_REQUEST',
|
704 | 'FETCH_USERS_SUCCESS',
|
705 | 'FETCH_USERS_FAILURE'
|
706 | )<string, User[], Error>();
|
707 |
|
708 | dispatch(fetchUsersAsync.request(params));
|
709 |
|
710 | dispatch(fetchUsersAsync.success(response));
|
711 |
|
712 | dispatch(fetchUsersAsync.failure(err));
|
713 |
|
714 | const fn = (
|
715 | a: AsyncActionCreator<
|
716 | ['FETCH_USERS_REQUEST', string],
|
717 | ['FETCH_USERS_SUCCESS', User[]],
|
718 | ['FETCH_USERS_FAILURE', Error]
|
719 | >
|
720 | ) => a;
|
721 | fn(fetchUsersAsync);
|
722 |
|
723 | // There is 4th optional argument to declare cancel action
|
724 | const fetchUsersAsync = createAsyncAction(
|
725 | 'FETCH_USERS_REQUEST',
|
726 | 'FETCH_USERS_SUCCESS',
|
727 | 'FETCH_USERS_FAILURE'
|
728 | 'FETCH_USERS_CANCEL'
|
729 | )<string, User[], Error, string>();
|
730 |
|
731 | dispatch(fetchUsersAsync.cancel('reason'));
|
732 |
|
733 | const fn = (
|
734 | a: AsyncActionCreator<
|
735 | ['FETCH_USERS_REQUEST', string],
|
736 | ['FETCH_USERS_SUCCESS', User[]],
|
737 | ['FETCH_USERS_FAILURE', Error],
|
738 | ['FETCH_USERS_CANCEL', string]
|
739 | >
|
740 | ) => a;
|
741 | fn(fetchUsersAsync);
|
742 | ```
|
743 |
|
744 | [⇧ back to top](#table-of-contents)
|
745 |
|
746 | ---
|
747 |
|
748 | ### Reducer-Creators API
|
749 |
|
750 | #### `createReducer`
|
751 |
|
752 | _Create a typesafe reducer_
|
753 |
|
754 | ```ts
|
755 | createReducer<TState, TRootAction>(initialState, handlersMap?)
|
756 | // or
|
757 | createReducer<TState, TRootAction>(initialState)
|
758 | .handleAction(actionCreator, reducer)
|
759 | .handleAction([actionCreator1, actionCreator2, ...actionCreatorN], reducer)
|
760 | .handleType(type, reducer)
|
761 | .handleType([type1, type2, ...typeN], reducer)
|
762 | ```
|
763 |
|
764 | Examples:
|
765 | [> Advanced Usage Examples](src/create-reducer.spec.ts)
|
766 |
|
767 | > **TIP:** You can use reducer API with a **type-free** syntax by [Extending internal types](#extending-internal-types-to-enable-type-free-syntax-with-createreducer), otherwise you'll have to pass generic type arguments like in below examples
|
768 | ```ts
|
769 | // type-free syntax doesn't require generic type arguments
|
770 | const counterReducer = createReducer(0, {
|
771 | ADD: (state, action) => state + action.payload,
|
772 | [getType(increment)]: (state, _) => state + 1,
|
773 | })
|
774 | ```
|
775 |
|
776 | **Object map style:**
|
777 | ```ts
|
778 | import { createReducer, getType } from 'typesafe-actions'
|
779 |
|
780 | type State = number;
|
781 | type Action = { type: 'ADD', payload: number } | { type: 'INCREMENT' };
|
782 |
|
783 | const counterReducer = createReducer<State, Action>(0, {
|
784 | ADD: (state, action) => state + action.payload,
|
785 | [getType(increment)]: (state, _) => state + 1,
|
786 | })
|
787 | ```
|
788 |
|
789 | **Chain API style:**
|
790 | ```ts
|
791 | // using action-creators
|
792 | const counterReducer = createReducer<State, Action>(0)
|
793 | .handleAction(add, (state, action) => state + action.payload)
|
794 | .handleAction(increment, (state, _) => state + 1)
|
795 |
|
796 | // handle multiple actions by using array
|
797 | .handleAction([add, increment], (state, action) =>
|
798 | state + (action.type === 'ADD' ? action.payload : 1)
|
799 | );
|
800 |
|
801 | // all the same scenarios are working when using type-constants
|
802 | const counterReducer = createReducer<State, Action>(0)
|
803 | .handleType('ADD', (state, action) => state + action.payload)
|
804 | .handleType('INCREMENT', (state, _) => state + 1);
|
805 | ```
|
806 |
|
807 | **Extend or compose reducers - every operation is completely typesafe:**
|
808 | ```ts
|
809 | const newCounterReducer = createReducer<State, Action>(0)
|
810 | .handleAction('SUBTRACT', (state, action) => state - action.payload)
|
811 | .handleAction('DECREMENT', (state, _) => state - 1);
|
812 |
|
813 | const bigReducer = createReducer<State, Action>(0, {
|
814 | ...counterReducer.handlers, // typesafe
|
815 | ...newCounterReducer.handlers, // typesafe
|
816 | SUBTRACT: decrementReducer.handlers.DECREMENT, // <= error, wrong type
|
817 | })
|
818 | ```
|
819 |
|
820 | [⇧ back to top](#table-of-contents)
|
821 |
|
822 | ---
|
823 |
|
824 | ### Action-Helpers API
|
825 |
|
826 | #### `getType`
|
827 |
|
828 | _Get the **type** property value (narrowed to literal type) of given enhanced action-creator._
|
829 |
|
830 | ```ts
|
831 | getType(actionCreator)
|
832 | ```
|
833 |
|
834 | [> Advanced Usage Examples](src/get-type.spec.ts)
|
835 |
|
836 | Examples:
|
837 | ```ts
|
838 | import { getType, createStandardAction } from 'typesafe-actions';
|
839 |
|
840 | const add = createStandardAction('ADD')<number>();
|
841 |
|
842 | // In switch reducer
|
843 | switch (action.type) {
|
844 | case getType(add):
|
845 | // action type is { type: "ADD"; payload: number; }
|
846 | return state + action.payload;
|
847 |
|
848 | default:
|
849 | return state;
|
850 | }
|
851 |
|
852 | // or with conditional statements
|
853 | if (action.type === getType(add)) {
|
854 | // action type is { type: "ADD"; payload: number; }
|
855 | }
|
856 | ```
|
857 |
|
858 | [⇧ back to top](#table-of-contents)
|
859 |
|
860 | #### `isActionOf`
|
861 |
|
862 | _Check if action is an instance of given enhanced action-creator(s)
|
863 | (it will narrow action type to a type of given action-creator(s))_
|
864 |
|
865 |
|
866 | > **WARNING**: Regular action creators and [action](#action) will not work with this helper
|
867 |
|
868 | ```ts
|
869 | // can be used as a binary function
|
870 | isActionOf(actionCreator, action)
|
871 | // or as a curried function
|
872 | isActionOf(actionCreator)(action)
|
873 | // also accepts an array
|
874 | isActionOf([actionCreator1, actionCreator2, ...actionCreatorN], action)
|
875 | // with its curried equivalent
|
876 | isActionOf([actionCreator1, actionCreator2, ...actionCreatorN])(action)
|
877 | ```
|
878 |
|
879 | Examples:
|
880 | [> Advanced Usage Examples](src/is-action-of.spec.ts)
|
881 |
|
882 | ```ts
|
883 | import { addTodo, removeTodo } from './todos-actions';
|
884 |
|
885 | // Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.)
|
886 | // - single action
|
887 | [action1, action2, ...actionN]
|
888 | .filter(isActionOf(addTodo)) // only actions with type `ADD` will pass
|
889 | .map((action) => {
|
890 | // action type is { type: "todos/ADD"; payload: Todo; }
|
891 | ...
|
892 |
|
893 | // - multiple actions
|
894 | [action1, action2, ...actionN]
|
895 | .filter(isActionOf([addTodo, removeTodo])) // only actions with type `ADD` or 'REMOVE' will pass
|
896 | .do((action) => {
|
897 | // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
|
898 | ...
|
899 |
|
900 | // With conditional statements
|
901 | // - single action
|
902 | if(isActionOf(addTodo, action)) {
|
903 | return iAcceptOnlyTodoType(action.payload);
|
904 | // action type is { type: "todos/ADD"; payload: Todo; }
|
905 | }
|
906 | // - multiple actions
|
907 | if(isActionOf([addTodo, removeTodo], action)) {
|
908 | return iAcceptOnlyTodoType(action.payload);
|
909 | // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
|
910 | }
|
911 | ```
|
912 |
|
913 | [⇧ back to top](#table-of-contents)
|
914 |
|
915 | #### `isOfType`
|
916 |
|
917 | _Check if action type property is equal given type-constant(s)
|
918 | (it will narrow action type to a type of given action-creator(s))_
|
919 |
|
920 | ```ts
|
921 | // can be used as a binary function
|
922 | isOfType(type, action)
|
923 | // or as curried function
|
924 | isOfType(type)(action)
|
925 | // also accepts an array
|
926 | isOfType([type1, type2, ...typeN], action)
|
927 | // with its curried equivalent
|
928 | isOfType([type1, type2, ...typeN])(action)
|
929 | ```
|
930 |
|
931 | Examples:
|
932 | [> Advanced Usage Examples](src/is-of-type.spec.ts)
|
933 |
|
934 | ```ts
|
935 | import { ADD, REMOVE } from './todos-types';
|
936 |
|
937 | // Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.)
|
938 | // - single action
|
939 | [action1, action2, ...actionN]
|
940 | .filter(isOfType(ADD)) // only actions with type `ADD` will pass
|
941 | .map((action) => {
|
942 | // action type is { type: "todos/ADD"; payload: Todo; }
|
943 | ...
|
944 |
|
945 | // - multiple actions
|
946 | [action1, action2, ...actionN]
|
947 | .filter(isOfType([ADD, REMOVE])) // only actions with type `ADD` or 'REMOVE' will pass
|
948 | .do((action) => {
|
949 | // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
|
950 | ...
|
951 |
|
952 | // With conditional statements
|
953 | // - single action
|
954 | if(isOfType(ADD, action)) {
|
955 | return iAcceptOnlyTodoType(action.payload);
|
956 | // action type is { type: "todos/ADD"; payload: Todo; }
|
957 | }
|
958 | // - multiple actions
|
959 | if(isOfType([ADD, REMOVE], action)) {
|
960 | return iAcceptOnlyTodoType(action.payload);
|
961 | // action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
|
962 | }
|
963 | ```
|
964 |
|
965 | [⇧ back to top](#table-of-contents)
|
966 |
|
967 | ---
|
968 |
|
969 | ### Type-Helpers API
|
970 | Below helper functions are very flexible generalizations, works great with nested structures and will cover numerous different use-cases.
|
971 |
|
972 | #### `ActionType`
|
973 |
|
974 | _Powerful type-helper that will infer union type from **import * as ...** or **action-creator map** object._
|
975 |
|
976 | ```ts
|
977 | import { ActionType } from 'typesafe-actions';
|
978 |
|
979 | // with "import * as ..."
|
980 | import * as todos from './actions';
|
981 | export type TodosAction = ActionType<typeof todos>;
|
982 | // TodosAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }
|
983 |
|
984 | // with nested action-creator map case
|
985 | const actions = {
|
986 | action1: createAction('action1'),
|
987 | nested: {
|
988 | action2: createAction('action2'),
|
989 | moreNested: {
|
990 | action3: createAction('action3'),
|
991 | },
|
992 | },
|
993 | };
|
994 | export type RootAction = ActionType<typeof actions>;
|
995 | // RootAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }
|
996 | ```
|
997 |
|
998 | [⇧ back to top](#table-of-contents)
|
999 |
|
1000 | #### `StateType`
|
1001 |
|
1002 | _Powerful type helper that will infer state object type from **reducer function** and **nested/combined reducers**._
|
1003 |
|
1004 | > **WARNING**: working with redux@4+ types
|
1005 |
|
1006 | ```ts
|
1007 | import { combineReducers } from 'redux';
|
1008 | import { StateType } from 'typesafe-actions';
|
1009 |
|
1010 | // with reducer function
|
1011 | const todosReducer = (state: Todo[] = [], action: TodosAction) => {
|
1012 | switch (action.type) {
|
1013 | case getType(todos.add):
|
1014 | return [...state, action.payload];
|
1015 | ...
|
1016 | export type TodosState = StateType<typeof todosReducer>;
|
1017 |
|
1018 | // with nested/combined reducers
|
1019 | const rootReducer = combineReducers({
|
1020 | router: routerReducer,
|
1021 | counters: countersReducer,
|
1022 | });
|
1023 | export type RootState = StateType<typeof rootReducer>;
|
1024 | ```
|
1025 |
|
1026 | [⇧ back to top](#table-of-contents)
|
1027 |
|
1028 | ---
|
1029 |
|
1030 | ## Migration Guides
|
1031 |
|
1032 | ### `v4.x.x` to `v5.x.x`
|
1033 |
|
1034 | **Breaking changes:**
|
1035 |
|
1036 | 1. In `v5` all the deprecated `v4` creator functions are available under `deprecated` named import to help with incremental migration.
|
1037 | ```ts
|
1038 | // before
|
1039 | import { createAction, createStandardAction, createCustomAction } from "typesafe-actions"
|
1040 |
|
1041 | // after
|
1042 | import { deprecated } from "typesafe-actions"
|
1043 | const { createAction, createStandardAction, createCustomAction } = deprecated;
|
1044 | ```
|
1045 |
|
1046 | 2. `createStandardAction` was renamed to `createAction` and `.map` method was removed in favor of simpler `redux-actions` style API.
|
1047 | ```ts
|
1048 | // before
|
1049 | const withMappedPayloadAndMeta = createStandardAction(
|
1050 | 'CREATE_STANDARD_ACTION'
|
1051 | ).map(({ username, message }: Notification) => ({
|
1052 | payload: `${username}: ${message}`,
|
1053 | meta: { username, message },
|
1054 | }));
|
1055 |
|
1056 | // after
|
1057 | const withMappedPayloadAndMeta = createAction(
|
1058 | 'CREATE_STANDARD_ACTION',
|
1059 | ({ username, message }: Notification) => `${username}: ${message}`, // payload creator
|
1060 | ({ username, message }: Notification) => ({ username, message }) // meta creator
|
1061 | )();
|
1062 | ```
|
1063 |
|
1064 | 3. `v4` version of `createAction` was removed. I suggest to refactor to use a new `createAction` as in point `2`, which was simplified and extended to support `redux-actions` style API.
|
1065 | ```ts
|
1066 | // before
|
1067 | const withPayloadAndMeta = createAction('CREATE_ACTION', resolve => {
|
1068 | return (id: number, token: string) => resolve(id, token);
|
1069 | });
|
1070 |
|
1071 | // after
|
1072 | const withPayloadAndMeta = createAction(
|
1073 | 'CREATE_ACTION',
|
1074 | (id: number, token: string) => id, // payload creator
|
1075 | (id: number, token: string) => token // meta creator
|
1076 | })();
|
1077 | ```
|
1078 |
|
1079 | 4. `createCustomAction` - API was greatly simplified, now it's used like this:
|
1080 | ```ts
|
1081 | // before
|
1082 | const add = createCustomAction('CUSTOM', type => {
|
1083 | return (first: number, second: number) => ({ type, customProp1: first, customProp2: second });
|
1084 | });
|
1085 |
|
1086 | // after
|
1087 | const add = createCustomAction(
|
1088 | 'CUSTOM',
|
1089 | (first: number, second: number) => ({ customProp1: first, customProp2: second })
|
1090 | );
|
1091 | ```
|
1092 |
|
1093 | 5. `AsyncActionCreator` should be just renamed to `AsyncActionCreatorBuilder`.
|
1094 | ```ts
|
1095 | // before
|
1096 | import { AsyncActionCreator } from "typesafe-actions"
|
1097 |
|
1098 | //after
|
1099 | import { AsyncActionCreatorBuilder } from "typesafe-actions"
|
1100 | ```
|
1101 |
|
1102 | ### `v3.x.x` to `v4.x.x`
|
1103 |
|
1104 | **No breaking changes!**
|
1105 |
|
1106 | ### `v2.x.x` to `v3.x.x`
|
1107 |
|
1108 | Minimal supported TypeScript `v3.1+`.
|
1109 |
|
1110 | ### `v1.x.x` to `v2.x.x`
|
1111 |
|
1112 | **Breaking changes:**
|
1113 |
|
1114 | 1. `createAction`
|
1115 | - In `v2` we provide a `createActionDeprecated` function compatible with `v1` `createAction` to help with incremental migration.
|
1116 |
|
1117 | ```ts
|
1118 | // in v1 we created action-creator like this:
|
1119 | const getTodo = createAction('GET_TODO',
|
1120 | (id: string, meta: string) => ({
|
1121 | type: 'GET_TODO',
|
1122 | payload: id,
|
1123 | meta: meta,
|
1124 | })
|
1125 | );
|
1126 |
|
1127 | getTodo('some_id', 'some_meta'); // { type: 'GET_TODO', payload: 'some_id', meta: 'some_meta' }
|
1128 |
|
1129 | // in v2 we offer few different options - please choose your preference
|
1130 | const getTodoNoHelpers = (id: string, meta: string) => action('GET_TODO', id, meta);
|
1131 |
|
1132 | const getTodoWithHelpers = createAction('GET_TODO', action => {
|
1133 | return (id: string, meta: string) => action(id, meta);
|
1134 | });
|
1135 |
|
1136 | const getTodoFSA = createStandardAction('GET_TODO')<string, string>();
|
1137 |
|
1138 | const getTodoCustom = createStandardAction('GET_TODO').map(
|
1139 | ({ id, meta }: { id: string; meta: string; }) => ({
|
1140 | payload: id,
|
1141 | meta,
|
1142 | })
|
1143 | );
|
1144 | ```
|
1145 |
|
1146 | [⇧ back to top](#table-of-contents)
|
1147 |
|
1148 | ### Migrating from `redux-actions` to `typesafe-actions`
|
1149 |
|
1150 | - createAction(s)
|
1151 |
|
1152 | ```ts
|
1153 | createAction(type, payloadCreator, metaCreator) => createStandardAction(type)() || createStandardAction(type).map(payloadMetaCreator)
|
1154 |
|
1155 | createActions() => // COMING SOON!
|
1156 | ```
|
1157 |
|
1158 | - handleAction(s)
|
1159 |
|
1160 | ```ts
|
1161 | handleAction(type, reducer, initialState) => createReducer(initialState).handleAction(type, reducer)
|
1162 |
|
1163 | handleActions(reducerMap, initialState) => createReducer(initialState, reducerMap)
|
1164 | ```
|
1165 |
|
1166 | > TIP: If migrating from JS -> TS, you can swap out action-creators from `redux-actions` with action-creators from `typesafe-actions` in your `handleActions` handlers. This works because the action-creators from `typesafe-actions` provide the same `toString` method implementation used by `redux-actions` to match actions to the correct reducer.
|
1167 |
|
1168 | - combineActions
|
1169 |
|
1170 | Not needed because each function in the API accept single value or array of values for action types or action creators.
|
1171 |
|
1172 | [⇧ back to top](#table-of-contents)
|
1173 |
|
1174 | ---
|
1175 |
|
1176 | ## Compatibility Notes
|
1177 |
|
1178 | **TypeScript support**
|
1179 |
|
1180 | - `5.X.X` - TypeScript v3.2+
|
1181 | - `4.X.X` - TypeScript v3.2+
|
1182 | - `3.X.X` - TypeScript v3.2+
|
1183 | - `2.X.X` - TypeScript v2.9+
|
1184 | - `1.X.X` - TypeScript v2.7+
|
1185 |
|
1186 | **Browser support**
|
1187 |
|
1188 | It's compatible with all modern browsers.
|
1189 |
|
1190 | For older browsers support (e.g. IE <= 11) and some mobile devices you need to provide the following polyfills:
|
1191 | - [Object.assign](https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Object/assign#Polyfill)
|
1192 | - [Array.prototype.includes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)
|
1193 |
|
1194 | **Recommended polyfill for IE**
|
1195 |
|
1196 | To provide the best compatibility please include a popular polyfill package in your application, such as `core-js` or `react-app-polyfill` for `create-react-app`.
|
1197 | Please check the `React` guidelines to learn how to do that: [LINK](https://reactjs.org/docs/javascript-environment-requirements.html)
|
1198 | A polyfill fo IE11 is included in our `/codesandbox` application.
|
1199 |
|
1200 | [⇧ back to top](#table-of-contents)
|
1201 |
|
1202 | ---
|
1203 |
|
1204 | ## Recipes
|
1205 |
|
1206 | ### Restrict Meta type in `action` creator
|
1207 | Using this recipe you can create an action creator with restricted Meta type with exact object shape.
|
1208 |
|
1209 | ```tsx
|
1210 | export type MetaType = {
|
1211 | analytics?: {
|
1212 | eventName: string;
|
1213 | };
|
1214 | };
|
1215 |
|
1216 | export const actionWithRestrictedMeta = <T extends string, P>(
|
1217 | type: T,
|
1218 | payload: P,
|
1219 | meta: MetaType
|
1220 | ) => action(type, payload, meta);
|
1221 |
|
1222 | export const validAction = (payload: string) =>
|
1223 | actionWithRestrictedMeta('type', payload, { analytics: { eventName: 'success' } }); // OK!
|
1224 |
|
1225 | export const invalidAction = (payload: string) =>
|
1226 | actionWithRestrictedMeta('type', payload, { analytics: { excessProp: 'no way!' } }); // Error
|
1227 | // Object literal may only specify known properties, and 'excessProp' does not exist in type '{ eventName: string; }
|
1228 | ```
|
1229 |
|
1230 | [⇧ back to top](#table-of-contents)
|
1231 |
|
1232 | ---
|
1233 |
|
1234 | ## Compare to others
|
1235 |
|
1236 | Here you can find out a detailed comparison of `typesafe-actions` to other solutions.
|
1237 |
|
1238 | ### `redux-actions`
|
1239 | Lets compare the 3 most common variants of action-creators (with type only, with payload and with payload + meta)
|
1240 |
|
1241 | Note: tested with "@types/redux-actions": "2.2.3"
|
1242 |
|
1243 | **- with type only (no payload)**
|
1244 |
|
1245 | ##### redux-actions
|
1246 | ```ts
|
1247 | const notify1 = createAction('NOTIFY');
|
1248 | // resulting type:
|
1249 | // () => {
|
1250 | // type: string;
|
1251 | // payload: void | undefined;
|
1252 | // error: boolean | undefined;
|
1253 | // }
|
1254 | ```
|
1255 |
|
1256 | > with `redux-actions` you can notice the redundant nullable `payload` property and literal type of `type` property is lost (discrimination of union type would not be possible)
|
1257 |
|
1258 | ##### typesafe-actions
|
1259 | ```ts
|
1260 | const notify1 = () => action('NOTIFY');
|
1261 | // resulting type:
|
1262 | // () => {
|
1263 | // type: "NOTIFY";
|
1264 | // }
|
1265 | ```
|
1266 |
|
1267 | > with `typesafe-actions` there is no excess nullable types and no excess properties and the action "type" property is containing a literal type
|
1268 |
|
1269 | **- with payload**
|
1270 |
|
1271 | ##### redux-actions
|
1272 | ```ts
|
1273 | const notify2 = createAction('NOTIFY',
|
1274 | (username: string, message?: string) => ({
|
1275 | message: `${username}: ${message || 'Empty!'}`,
|
1276 | })
|
1277 | );
|
1278 | // resulting type:
|
1279 | // (t1: string) => {
|
1280 | // type: string;
|
1281 | // payload: { message: string; } | undefined;
|
1282 | // error: boolean | undefined;
|
1283 | // }
|
1284 | ```
|
1285 |
|
1286 | > first the optional `message` parameter is lost, `username` semantic argument name is changed to some generic `t1`, `type` property is widened once again and `payload` is nullable because of broken inference
|
1287 |
|
1288 | ##### typesafe-actions
|
1289 | ```ts
|
1290 | const notify2 = (username: string, message?: string) => action(
|
1291 | 'NOTIFY',
|
1292 | { message: `${username}: ${message || 'Empty!'}` },
|
1293 | );
|
1294 | // resulting type:
|
1295 | // (username: string, message?: string | undefined) => {
|
1296 | // type: "NOTIFY";
|
1297 | // payload: { message: string; };
|
1298 | // }
|
1299 | ```
|
1300 |
|
1301 | > `typesafe-actions` infer very precise resulting type, notice working optional parameters and semantic argument names are preserved which is really important for great intellisense experience
|
1302 |
|
1303 | **- with payload and meta**
|
1304 |
|
1305 | ##### redux-actions
|
1306 | ```ts
|
1307 | const notify3 = createAction('NOTIFY',
|
1308 | (username: string, message?: string) => (
|
1309 | { message: `${username}: ${message || 'Empty!'}` }
|
1310 | ),
|
1311 | (username: string, message?: string) => (
|
1312 | { username, message }
|
1313 | )
|
1314 | );
|
1315 | // resulting type:
|
1316 | // (...args: any[]) => {
|
1317 | // type: string;
|
1318 | // payload: { message: string; } | undefined;
|
1319 | // meta: { username: string; message: string | undefined; };
|
1320 | // error: boolean | undefined;
|
1321 | // }
|
1322 | ```
|
1323 |
|
1324 | > this time we got a completely broken arguments arity with no type-safety because of `any` type with all the earlier issues
|
1325 |
|
1326 | ##### typesafe-actions
|
1327 | ```ts
|
1328 | /**
|
1329 | * typesafe-actions
|
1330 | */
|
1331 | const notify3 = (username: string, message?: string) => action(
|
1332 | 'NOTIFY',
|
1333 | { message: `${username}: ${message || 'Empty!'}` },
|
1334 | { username, message },
|
1335 | );
|
1336 | // resulting type:
|
1337 | // (username: string, message?: string | undefined) => {
|
1338 | // type: "NOTIFY";
|
1339 | // payload: { message: string; };
|
1340 | // meta: { username: string; message: string | undefined; };
|
1341 | // }
|
1342 | ```
|
1343 |
|
1344 | > `typesafe-actions` never fail to `any` type, even with this advanced scenario all types are correct and provide complete type-safety and excellent developer experience
|
1345 |
|
1346 | [⇧ back to top](#table-of-contents)
|
1347 |
|
1348 | ---
|
1349 |
|
1350 | ## Motivation
|
1351 |
|
1352 | When I started to combine Redux with TypeScript, I was trying to use [redux-actions](https://redux-actions.js.org/) to reduce the maintainability cost and boilerplate of **action-creators**. Unfortunately, the results were intimidating: incorrect type signatures and broken type-inference cascading throughout the entire code-base [(click here for a detailed comparison)](#redux-actions).
|
1353 |
|
1354 | Existing solutions in the wild have been either **too verbose because of redundant type annotations** (hard to maintain) or **used classes** (hinders readability and requires using the **new** keyword 😱)
|
1355 |
|
1356 | **So I created `typesafe-actions` to address all of the above pain points.**
|
1357 |
|
1358 | The core idea was to design an API that would mostly use the power of TypeScript **type-inference** 💪 to lift the "maintainability burden" of type annotations. In addition, I wanted to make it "look and feel" as close as possible to the idiomatic JavaScript ❤️ , so we don't have to write the redundant type annotations that which will create additional noise in your code.
|
1359 |
|
1360 | [⇧ back to top](#table-of-contents)
|
1361 |
|
1362 | ---
|
1363 |
|
1364 | ## Contributing
|
1365 |
|
1366 | You can help make this project better by contributing. If you're planning to contribute please make sure to check our contributing guide: [CONTRIBUTING.md](/CONTRIBUTING.md)
|
1367 |
|
1368 | [⇧ back to top](#table-of-contents)
|
1369 |
|
1370 | ---
|
1371 |
|
1372 | ## Funding Issues
|
1373 |
|
1374 | You can also help by funding issues.
|
1375 | Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.
|
1376 |
|
1377 | I highly recommend to add a bounty to the issue that you're waiting for to increase priority and attract contributors willing to work on it.
|
1378 |
|
1379 | [![Let's fund issues in this repository](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/110746954)
|
1380 |
|
1381 | [⇧ back to top](#table-of-contents)
|
1382 |
|
1383 | ---
|
1384 |
|
1385 | ## License
|
1386 |
|
1387 | [MIT License](/LICENSE)
|
1388 |
|
1389 | Copyright (c) 2017 Piotr Witek <piotrek.witek@gmail.com> (http://piotrwitek.github.io)
|