UNPKG

49.2 kBMarkdownView Raw
1<div align="center">
2
3# typesafe-actions
4
5Typesafe utilities designed to reduce types **verbosity**
6and **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<!-- _Make a one time or a monthly donation to join [supporters](https://www.buymeacoffee.com/piotrekwitek)_ -->
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<!-- _Or become a [sponsor](..) to get your logo with a link on our README_ -->
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<!-- START doctoc generated TOC please keep comment here to allow auto update -->
71<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
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<!-- END doctoc generated TOC please keep comment here to allow auto update -->
124
125<hr/>
126
127## Installation
128
129```bash
130# NPM
131npm install typesafe-actions
132
133# YARN
134yarn 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
143To 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
161export const ADD = prefix + 'ADD'; // => string
162export const ADD = `${prefix}/ADD`; // => string
163export default {
164 ADD: '@prefix/ADD', // => string
165}
166
167// Correct usage
168export const ADD = '@prefix/ADD'; // => '@prefix/ADD'
169export const TOGGLE = '@prefix/TOGGLE'; // => '@prefix/TOGGLE'
170export 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
179Different 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
184Important property is that resulting **action-creator** will have a variadic number of arguments and preserve their semantic names `(id, title, amount, etc...)`.
185
186This two creators are very similar and the only real difference is that `action` **WILL NOT WORK** with **action-helpers**.
187
188```ts
189import { action, createAction } from 'typesafe-actions';
190
191export 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
194export 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
202This 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
204It 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
209import { createStandardAction } from 'typesafe-actions';
210
211export const toggle = createStandardAction('todos/TOGGLE')<string>();
212// toggle: (payload: string) => { type: "todos/TOGGLE"; payload: string; }
213
214export 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
224This 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
227import { createCustomAction } from 'typesafe-actions';
228
229const 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`
243import { RouterAction, LocationChangeAction } from 'react-router-redux';
244import { TodosAction } from '../features/todos';
245
246type ReactRouterAction = RouterAction | LocationChangeAction;
247
248export type RootAction =
249 | ReactRouterAction
250 | TodosAction;
251```
252
253[⇧ back to top](#table-of-contents)
254
255### Action Helpers
256
257Now 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
259Important 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
263Instead 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
265The 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
275Then 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
279import { isActionOf } from 'typesafe-actions';
280
281import { add } from './actions';
282
283const 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
298Alternatively if your team prefers to use regular **type-constants** you can still do that.
299
300We have an equivalent helper (`isOfType`) which accept **type-constants** as parameter providing the same functionality.
301
302```ts
303// epics.ts
304import { isOfType } from 'typesafe-actions';
305
306import { ADD } from './constants';
307
308const 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
323import { isActionOf, isOfType } from 'typesafe-actions';
324
325if (isActionOf(actions.add, action)) {
326 // here action is narrowed to: { type: "todos/ADD"; payload: Todo; }
327}
328// or with type constants
329if (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
340We 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
344import { StateType, ActionType } from 'typesafe-actions';
345
346export type RootAction = ActionType<typeof import('./actions').default>;
347
348declare module 'typesafe-actions' {
349 interface Types {
350 RootAction: RootAction;
351 }
352}
353
354// now you can use
355createReducer(...)
356// instead of
357createReducer<State, Action>(...)
358```
359
360#### Using createReducer API with type-free syntax
361
362We can prevent a lot of boilerplate code and type errors using this powerfull and completely typesafe API.
363
364Using handleAction chain API:
365```ts
366// using action-creators
367const 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
380const counterReducer = createReducer(0)
381 .handleAction('ADD', (state, action) => state + action.payload)
382 .handleAction('INCREMENT', (state, _) => state + 1);
383
384counterReducer(0, add(4)); // => 4
385counterReducer(0, increment()); // => 1
386```
387
388#### Alternative usage with regular switch reducer
389
390First 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
393import { ActionType } from 'typesafe-actions';
394
395import * as todos from './actions';
396export type TodosAction = ActionType<typeof todos>;
397```
398
399Now we define a regular reducer function by annotating `state` and `action` arguments with their respective types (`TodosAction` for action type).
400
401```ts
402export default (state: Todo[] = [], action: TodosAction) => {
403```
404
405Now 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
421To 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
423To 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`.
424This 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
428import { createAsyncAction } from 'typesafe-actions';
429
430const 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
438import { fetchTodosAsync } from './actions';
439
440const 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
453With 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
455Typescript 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
461import { createAsyncAction, createReducer } from 'typesafe-actions';
462import { put, call, takeEvery } from 'redux-saga/effetcs';
463
464// Create the set of async actions
465const fetchTodosAsync = createAsyncAction(
466 'FETCH_TODOS_REQUEST',
467 'FETCH_TODOS_SUCCESS',
468 'FETCH_TODOS_FAILURE'
469)<string, Todo[], Error>();
470
471// Handle request saga
472function* 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
483function* mainSaga() {
484 yield all([
485 takeEvery(fetchTodosAsync.request, addTodoSaga),
486 ]);
487}
488
489// Handle success reducer
490export 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
510action(type, payload?, meta?, error?)
511```
512
513Examples:
514[> Advanced Usage Examples](src/action.spec.ts)
515
516```ts
517const increment = () => action('INCREMENT');
518// { type: 'INCREMENT'; }
519
520const createUser = (id: number, name: string) =>
521 action('CREATE_USER', { id, name });
522// { type: 'CREATE_USER'; payload: { id: number; name: string }; }
523
524const 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
532const 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
542createAction(type)
543createAction(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
549Examples:
550[> Advanced Usage Examples](src/create-action.spec.ts)
551
552```ts
553import { createAction } from 'typesafe-actions';
554
555// - with type only
556const increment = createAction('INCREMENT');
557dispatch(increment());
558// { type: 'INCREMENT' };
559
560// - with type and payload
561const add = createAction('ADD', action => {
562 return (amount: number) => action(amount);
563});
564dispatch(add(10));
565// { type: 'ADD', payload: number }
566
567// - with type and meta
568const getTodos = createAction('GET_TODOS', action => {
569 return (params: Params) => action(undefined, params);
570});
571dispatch(getTodos('some_meta'));
572// { type: 'GET_TODOS', meta: Params }
573
574// - and finally with type, payload and meta
575const getTodo = createAction('GET_TODO', action => {
576 return (id: string, meta: string) => action(id, meta);
577});
578dispatch(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
592createStandardAction(type)()
593createStandardAction(type)<TPayload, TMeta?>()
594createStandardAction(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
599Examples:
600[> Advanced Usage Examples](src/create-standard-action.spec.ts)
601
602```ts
603import { createStandardAction } from 'typesafe-actions';
604
605// Very concise with use of generic type arguments
606// - with type only
607const increment = createStandardAction('INCREMENT')();
608const increment = createStandardAction('INCREMENT')<undefined>();
609increment(); // { type: 'INCREMENT' } (no parameters are required)
610
611
612// - with type and payload
613const add = createStandardAction('ADD')<number>();
614add(10); // { type: 'ADD', payload: number }
615
616// - with type and meta
617const getData = createStandardAction('GET_DATA')<undefined, string>();
618getData(undefined, 'meta'); // { type: 'GET_DATA', meta: string }
619
620// - and finally with type, payload and meta
621const getData = createStandardAction('GET_DATA')<number, string>();
622getData(1, 'meta'); // { type: 'GET_DATA', payload: number, meta: string }
623
624// Can map payload and meta arguments to a custom action object
625const 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
634dispatch(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
647createCustomAction(type, type => {
648 return (namedArg1, namedArg2, ...namedArgN) => ({ type, customProp1, customProp2, ...customPropN })
649})
650```
651
652Examples:
653[> Advanced Usage Examples](src/create-action-with-type.spec.ts)
654
655```ts
656import { createCustomAction } from 'typesafe-actions';
657
658const add = createCustomAction('CUSTOM', type => {
659 return (first: number, second: number) => ({ type, customProp1: first, customProp2: second });
660});
661
662dispatch(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
673createAsyncAction(
674 requestType, successType, failureType, cancelType?
675)<TRequestPayload, TSuccessPayload, TFailurePayload, TCancelPayload?>()
676```
677
678##### `AsyncActionCreator`
679
680```ts
681type 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
696Examples:
697[> Advanced Usage Examples](src/create-async-action.spec.ts)
698
699```ts
700import { createAsyncAction, AsyncActionCreator } from 'typesafe-actions';
701
702const fetchUsersAsync = createAsyncAction(
703 'FETCH_USERS_REQUEST',
704 'FETCH_USERS_SUCCESS',
705 'FETCH_USERS_FAILURE'
706)<string, User[], Error>();
707
708dispatch(fetchUsersAsync.request(params));
709
710dispatch(fetchUsersAsync.success(response));
711
712dispatch(fetchUsersAsync.failure(err));
713
714const fn = (
715 a: AsyncActionCreator<
716 ['FETCH_USERS_REQUEST', string],
717 ['FETCH_USERS_SUCCESS', User[]],
718 ['FETCH_USERS_FAILURE', Error]
719 >
720) => a;
721fn(fetchUsersAsync);
722
723// There is 4th optional argument to declare cancel action
724const fetchUsersAsync = createAsyncAction(
725 'FETCH_USERS_REQUEST',
726 'FETCH_USERS_SUCCESS',
727 'FETCH_USERS_FAILURE'
728 'FETCH_USERS_CANCEL'
729)<string, User[], Error, string>();
730
731dispatch(fetchUsersAsync.cancel('reason'));
732
733const 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;
741fn(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
755createReducer<TState, TRootAction>(initialState, handlersMap?)
756// or
757createReducer<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
764Examples:
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
770const 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
778import { createReducer, getType } from 'typesafe-actions'
779
780type State = number;
781type Action = { type: 'ADD', payload: number } | { type: 'INCREMENT' };
782
783const 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
792const 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
802const 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
809const newCounterReducer = createReducer<State, Action>(0)
810 .handleAction('SUBTRACT', (state, action) => state - action.payload)
811 .handleAction('DECREMENT', (state, _) => state - 1);
812
813const 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
831getType(actionCreator)
832```
833
834[> Advanced Usage Examples](src/get-type.spec.ts)
835
836Examples:
837```ts
838import { getType, createStandardAction } from 'typesafe-actions';
839
840const add = createStandardAction('ADD')<number>();
841
842// In switch reducer
843switch (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
853if (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
870isActionOf(actionCreator, action)
871// or as a curried function
872isActionOf(actionCreator)(action)
873// also accepts an array
874isActionOf([actionCreator1, actionCreator2, ...actionCreatorN], action)
875// with its curried equivalent
876isActionOf([actionCreator1, actionCreator2, ...actionCreatorN])(action)
877```
878
879Examples:
880[> Advanced Usage Examples](src/is-action-of.spec.ts)
881
882```ts
883import { 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
902if(isActionOf(addTodo, action)) {
903 return iAcceptOnlyTodoType(action.payload);
904 // action type is { type: "todos/ADD"; payload: Todo; }
905}
906// - multiple actions
907if(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
922isOfType(type, action)
923// or as curried function
924isOfType(type)(action)
925// also accepts an array
926isOfType([type1, type2, ...typeN], action)
927// with its curried equivalent
928isOfType([type1, type2, ...typeN])(action)
929```
930
931Examples:
932[> Advanced Usage Examples](src/is-of-type.spec.ts)
933
934```ts
935import { 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
954if(isOfType(ADD, action)) {
955 return iAcceptOnlyTodoType(action.payload);
956 // action type is { type: "todos/ADD"; payload: Todo; }
957}
958// - multiple actions
959if(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
970Below 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
977import { ActionType } from 'typesafe-actions';
978
979// with "import * as ..."
980import * as todos from './actions';
981export type TodosAction = ActionType<typeof todos>;
982// TodosAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }
983
984// with nested action-creator map case
985const actions = {
986 action1: createAction('action1'),
987 nested: {
988 action2: createAction('action2'),
989 moreNested: {
990 action3: createAction('action3'),
991 },
992 },
993};
994export 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
1007import { combineReducers } from 'redux';
1008import { StateType } from 'typesafe-actions';
1009
1010// with reducer function
1011const todosReducer = (state: Todo[] = [], action: TodosAction) => {
1012 switch (action.type) {
1013 case getType(todos.add):
1014 return [...state, action.payload];
1015 ...
1016export type TodosState = StateType<typeof todosReducer>;
1017
1018// with nested/combined reducers
1019const rootReducer = combineReducers({
1020 router: routerReducer,
1021 counters: countersReducer,
1022});
1023export 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
10361. In `v5` all the deprecated `v4` creator functions are available under `deprecated` named import to help with incremental migration.
1037```ts
1038// before
1039import { createAction, createStandardAction, createCustomAction } from "typesafe-actions"
1040
1041// after
1042import { deprecated } from "typesafe-actions"
1043const { createAction, createStandardAction, createCustomAction } = deprecated;
1044```
1045
10462. `createStandardAction` was renamed to `createAction` and `.map` method was removed in favor of simpler `redux-actions` style API.
1047```ts
1048// before
1049const withMappedPayloadAndMeta = createStandardAction(
1050 'CREATE_STANDARD_ACTION'
1051).map(({ username, message }: Notification) => ({
1052 payload: `${username}: ${message}`,
1053 meta: { username, message },
1054}));
1055
1056// after
1057const 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
10643. `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
1067const withPayloadAndMeta = createAction('CREATE_ACTION', resolve => {
1068 return (id: number, token: string) => resolve(id, token);
1069});
1070
1071// after
1072const withPayloadAndMeta = createAction(
1073 'CREATE_ACTION',
1074 (id: number, token: string) => id, // payload creator
1075 (id: number, token: string) => token // meta creator
1076})();
1077```
1078
10794. `createCustomAction` - API was greatly simplified, now it's used like this:
1080```ts
1081// before
1082const add = createCustomAction('CUSTOM', type => {
1083 return (first: number, second: number) => ({ type, customProp1: first, customProp2: second });
1084});
1085
1086// after
1087const add = createCustomAction(
1088 'CUSTOM',
1089 (first: number, second: number) => ({ customProp1: first, customProp2: second })
1090);
1091```
1092
10935. `AsyncActionCreator` should be just renamed to `AsyncActionCreatorBuilder`.
1094```ts
1095// before
1096import { AsyncActionCreator } from "typesafe-actions"
1097
1098//after
1099import { 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
1108Minimal supported TypeScript `v3.1+`.
1109
1110### `v1.x.x` to `v2.x.x`
1111
1112**Breaking changes:**
1113
11141. `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:
1119const getTodo = createAction('GET_TODO',
1120 (id: string, meta: string) => ({
1121 type: 'GET_TODO',
1122 payload: id,
1123 meta: meta,
1124 })
1125);
1126
1127getTodo('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
1130const getTodoNoHelpers = (id: string, meta: string) => action('GET_TODO', id, meta);
1131
1132const getTodoWithHelpers = createAction('GET_TODO', action => {
1133 return (id: string, meta: string) => action(id, meta);
1134});
1135
1136const getTodoFSA = createStandardAction('GET_TODO')<string, string>();
1137
1138const 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
1153createAction(type, payloadCreator, metaCreator) => createStandardAction(type)() || createStandardAction(type).map(payloadMetaCreator)
1154
1155createActions() => // COMING SOON!
1156```
1157
1158- handleAction(s)
1159
1160```ts
1161handleAction(type, reducer, initialState) => createReducer(initialState).handleAction(type, reducer)
1162
1163handleActions(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
1170Not 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
1188It's compatible with all modern browsers.
1189
1190For 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
1196To 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`.
1197Please check the `React` guidelines to learn how to do that: [LINK](https://reactjs.org/docs/javascript-environment-requirements.html)
1198A 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
1207Using this recipe you can create an action creator with restricted Meta type with exact object shape.
1208
1209```tsx
1210export type MetaType = {
1211 analytics?: {
1212 eventName: string;
1213 };
1214};
1215
1216export const actionWithRestrictedMeta = <T extends string, P>(
1217 type: T,
1218 payload: P,
1219 meta: MetaType
1220) => action(type, payload, meta);
1221
1222export const validAction = (payload: string) =>
1223 actionWithRestrictedMeta('type', payload, { analytics: { eventName: 'success' } }); // OK!
1224
1225export 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
1236Here you can find out a detailed comparison of `typesafe-actions` to other solutions.
1237
1238### `redux-actions`
1239Lets compare the 3 most common variants of action-creators (with type only, with payload and with payload + meta)
1240
1241Note: tested with "@types/redux-actions": "2.2.3"
1242
1243**- with type only (no payload)**
1244
1245##### redux-actions
1246```ts
1247const 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
1260const 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
1273const 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
1290const 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
1307const 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 */
1331const 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
1352When 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
1354Existing 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
1358The 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
1366You 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
1374You can also help by funding issues.
1375Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.
1376
1377I 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
1389Copyright (c) 2017 Piotr Witek <piotrek.witek@gmail.com> (http://piotrwitek.github.io)