Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/piotrwitek/typesafe-actions

Typesafe utilities for "action-creators" in Redux / Flux Architecture
https://github.com/piotrwitek/typesafe-actions

action-creator javascript redux redux-actions static-typing typescript

Last synced: 3 days ago
JSON representation

Typesafe utilities for "action-creators" in Redux / Flux Architecture

Awesome Lists containing this project

README

        

# typesafe-actions

Typesafe utilities designed to reduce types **verbosity**
and **complexity** in Redux Architecture.

_This library is part of the [React & Redux TypeScript Guide](https://github.com/piotrwitek/react-redux-typescript-guide)_ ecosystem :book:

[![Latest Stable Version](https://img.shields.io/npm/v/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
[![NPM Downloads](https://img.shields.io/npm/dm/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
[![NPM Downloads](https://img.shields.io/npm/dt/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)
[![Bundlephobia Size](https://img.shields.io/bundlephobia/minzip/typesafe-actions.svg)](https://www.npmjs.com/package/typesafe-actions)

[![Build Status](https://semaphoreci.com/api/v1/piotrekwitek/typesafe-actions/branches/master/shields_badge.svg)](https://semaphoreci.com/piotrekwitek/typesafe-actions)
[![Dependency Status](https://img.shields.io/david/piotrwitek/typesafe-actions.svg)](https://david-dm.org/piotrwitek/typesafe-actions)
[![License](https://img.shields.io/npm/l/typesafe-actions.svg?style=flat)](https://david-dm.org/piotrwitek/typesafe-actions?type=peer)
[![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/typesafe-actions)

_Found it useful? Want more updates?_

[**Show your support by giving a :star:**](https://github.com/piotrwitek/typesafe-actions/stargazers)


Buy Me a Coffee


Become a Patron



### **What's new?**

:tada: _Now updated to support **TypeScript v3.7**_ :tada:

:warning: Library was recently updated to v5 :warning:

*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).*




### **Features**
- Easily create completely typesafe [Actions](#action-creators-api) or even [Async Actions](#createasyncaction)
- No boilerplate and completely typesafe [Reducers](#reducer-creators-api)
- Game-changing [Helper Types](#type-helpers-api) for Redux

### **Examples**

- Todo-App playground: [Codesandbox](https://codesandbox.io/s/github/piotrwitek/typesafe-actions/tree/master/codesandbox)
- React, Redux, TypeScript - RealWorld App: [Github](https://github.com/piotrwitek/react-redux-typescript-realworld-app) | [Demo](https://react-redux-typescript-realworld-app.netlify.com/)

### **Goals**

- **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)
- **Optimized** - distribution packages bundled in 3 different formats (`cjs`, `esm` and `umd`) with separate bundles for dev & prod (same as `react`)
- **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
- **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`

---

## Table of Contents

- [Installation](#installation)
- [Tutorial v4 (v5 is WIP #188)](#tutorial-v4-v5-is-wip-188)
- [Constants](#constants)
- [Actions](#actions)
- [1. Basic actions](#1-basic-actions)
- [2. FSA compliant actions](#2-fsa-compliant-actions)
- [3. Custom actions (non-standard use-cases)](#3-custom-actions-non-standard-use-cases)
- [Action Helpers](#action-helpers)
- [Using action-creators instances instead of type-constants](#using-action-creators-instances-instead-of-type-constants)
- [Using regular type-constants](#using-regular-type-constants)
- [Reducers](#reducers)
- [Extending internal types to enable type-free syntax with `createReducer`](#extending-internal-types-to-enable-type-free-syntax-with-createreducer)
- [Using createReducer API with type-free syntax](#using-createreducer-api-with-type-free-syntax)
- [Alternative usage with regular switch reducer](#alternative-usage-with-regular-switch-reducer)
- [Async-Flows](#async-flows)
- [With `redux-observable` epics](#with-redux-observable-epics)
- [With `redux-saga` sagas](#with-redux-saga-sagas)
- [API Docs v4 (v5 is WIP #189)](#api-docs-v4-v5-is-wip-189)
- [Action-Creators API](#action-creators-api)
- [`action`](#action)
- [`createAction`](#createaction)
- [`createStandardAction`](#createstandardaction)
- [`createCustomAction`](#createcustomaction)
- [`createAsyncAction`](#createasyncaction)
- [Reducer-Creators API](#reducer-creators-api)
- [`createReducer`](#createreducer)
- [Action-Helpers API](#action-helpers-api)
- [`getType`](#gettype)
- [`isActionOf`](#isactionof)
- [`isOfType`](#isoftype)
- [Type-Helpers API](#type-helpers-api)
- [`ActionType`](#actiontype)
- [`StateType`](#statetype)
- [Migration Guides](#migration-guides)
- [`v4.x.x` to `v5.x.x`](#v4xx-to-v5xx)
- [`v3.x.x` to `v4.x.x`](#v3xx-to-v4xx)
- [`v2.x.x` to `v3.x.x`](#v2xx-to-v3xx)
- [`v1.x.x` to `v2.x.x`](#v1xx-to-v2xx)
- [Migrating from `redux-actions` to `typesafe-actions`](#migrating-from-redux-actions-to-typesafe-actions)
- [Compatibility Notes](#compatibility-notes)
- [Recipes](#recipes)
- [Restrict Meta type in `action` creator](#restrict-meta-type-in-action-creator)
- [Compare to others](#compare-to-others)
- [`redux-actions`](#redux-actions)
- [Motivation](#motivation)
- [Contributing](#contributing)
- [Funding Issues](#funding-issues)
- [License](#license)


## Installation

```bash
# NPM
npm install typesafe-actions

# YARN
yarn add typesafe-actions
```

[⇧ back to top](#table-of-contents)

---

## Tutorial v4 (v5 is WIP [#188](https://github.com/piotrwitek/typesafe-actions/issues/188))

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:

> **WARNING**
> 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).

[⇧ back to top](#table-of-contents)

### Constants

> **RECOMMENDATION:**
> 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.

**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.

```ts
// Example file: './constants.ts'

// WARNING: Incorrect usage
export const ADD = prefix + 'ADD'; // => string
export const ADD = `${prefix}/ADD`; // => string
export default {
ADD: '@prefix/ADD', // => string
}

// Correct usage
export const ADD = '@prefix/ADD'; // => '@prefix/ADD'
export const TOGGLE = '@prefix/TOGGLE'; // => '@prefix/TOGGLE'
export default ({
ADD: '@prefix/ADD', // => '@prefix/ADD'
} as const) // working in TS v3.4 and above => https://github.com/Microsoft/TypeScript/pull/29510
```

[⇧ back to top](#table-of-contents)

### Actions

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.

#### 1. Basic actions
`action` and `createAction` are creators that can create **actions** with predefined properties ({ type, payload, meta }). This makes them concise but also opinionated.

Important property is that resulting **action-creator** will have a variadic number of arguments and preserve their semantic names `(id, title, amount, etc...)`.

These two creators are very similar and the only real difference is that `action` **WILL NOT WORK** with **action-helpers**.

```ts
import { action, createAction } from 'typesafe-actions';

export const add = (title: string) => action('todos/ADD', { id: cuid(), title, completed: false });
// add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }

export const add = createAction('todos/ADD', action => {
// Note: "action" callback does not need "type" parameter
return (title: string) => action({ id: cuid(), title, completed: false });
});
// add: (title: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }
```

#### 2. FSA compliant actions
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.

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.

> **TIP**: This creator is the most compatible with `redux-actions` in case you are migrating.

```ts
import { createStandardAction } from 'typesafe-actions';

export const toggle = createStandardAction('todos/TOGGLE')();
// toggle: (payload: string) => { type: "todos/TOGGLE"; payload: string; }

export const add = createStandardAction('todos/ADD').map(
(title: string) => ({
payload: { id: cuid(), title, completed: false },
})
);
// add: (payload: string) => { type: "todos/ADD"; payload: { id: string, title: string, completed: boolean; }; }
```

#### 3. Custom actions (non-standard use-cases)

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.

```ts
import { createCustomAction } from 'typesafe-actions';

const add = createCustomAction('todos/ADD', type => {
return (title: string) => ({ type, id: cuid(), title, completed: false });
});
// add: (title: string) => { type: "todos/ADD"; id: string; title: string; completed: boolean; }
```

> **TIP**: For more examples please check the [API Docs](#table-of-contents).

> **RECOMMENDATION**
> 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.

```ts
// types.d.ts
// example of including `react-router` actions in `RootAction`
import { RouterAction, LocationChangeAction } from 'react-router-redux';
import { TodosAction } from '../features/todos';

type ReactRouterAction = RouterAction | LocationChangeAction;

export type RootAction =
| ReactRouterAction
| TodosAction;
```

[⇧ back to top](#table-of-contents)

### Action Helpers

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.

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.

#### Using action-creators instances instead of type-constants

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.

The most common one is `getType`, which is useful for regular reducer switch cases:

```ts
switch (action.type) {
case getType(todos.add):
// below action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
return [...state, action.payload];
...
```

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).

```ts
// epics.ts
import { isActionOf } from 'typesafe-actions';

import { add } from './actions';

const addTodoToast: Epic = (action$, state$, { toastService }) =>
action$.pipe(
filter(isActionOf(add)),
tap(action => { // here action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
toastService.success(...);
})
...

// Works with multiple actions! (with type-safety up to 5)
action$.pipe(
filter(isActionOf([add, toggle])) // here action type is narrowed to a smaller union:
// { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
```

#### Using regular type-constants
Alternatively if your team prefers to use regular **type-constants** you can still do that.

We have an equivalent helper (`isOfType`) which accept **type-constants** as parameter providing the same functionality.

```ts
// epics.ts
import { isOfType } from 'typesafe-actions';

import { ADD } from './constants';

const addTodoToast: Epic = (action$, state$, { toastService }) =>
action$.pipe(
filter(isOfType(ADD)),
tap(action => { // here action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
...

// Works with multiple actions! (with type-safety up to 5)
action$.pipe(
filter(isOfType([ADD, TOGGLE])) // here action type is narrowed to a smaller union:
// { type: "todos/ADD"; payload: Todo; } | { type: "todos/TOGGLE"; payload: string; }
```

> **TIP:** you can use action-helpers with other types of conditional statements.

```ts
import { isActionOf, isOfType } from 'typesafe-actions';

if (isActionOf(actions.add, action)) {
// here action is narrowed to: { type: "todos/ADD"; payload: Todo; }
}
// or with type constants
if (isOfType(types.ADD, action)) {
// here action is narrowed to: { type: "todos/ADD"; payload: Todo; }
}
```

[⇧ back to top](#table-of-contents)

### Reducers

#### Extending internal types to enable type-free syntax with `createReducer`

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:

```ts
// types.d.ts
import { ActionType } from 'typesafe-actions';

export type RootAction = ActionType;

declare module 'typesafe-actions' {
interface Types {
RootAction: RootAction;
}
}

// now you can use
createReducer(...)
// instead of
createReducer(...)
```

#### Using createReducer API with type-free syntax

We can prevent a lot of boilerplate code and type errors using this powerful and completely typesafe API.

Using handleAction chain API:
```ts
// using action-creators
const counterReducer = createReducer(0)
// state and action type is automatically inferred and return type is validated to be exact type
.handleAction(add, (state, action) => state + action.payload)
.handleAction(add, ... // <= error is shown on duplicated or invalid actions
.handleAction(increment, (state, _) => state + 1)
.handleAction(... // <= error is shown when all actions are handled

// or handle multiple actions using array
.handleAction([add, increment], (state, action) =>
state + (action.type === 'ADD' ? action.payload : 1)
);

// all the same scenarios are working when using type-constants
const counterReducer = createReducer(0)
.handleAction('ADD', (state, action) => state + action.payload)
.handleAction('INCREMENT', (state, _) => state + 1);

counterReducer(0, add(4)); // => 4
counterReducer(0, increment()); // => 1
```

#### Alternative usage with regular switch reducer

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**.

```ts
import { ActionType } from 'typesafe-actions';

import * as todos from './actions';
export type TodosAction = ActionType;
```

Now we define a regular reducer function by annotating `state` and `action` arguments with their respective types (`TodosAction` for action type).

```ts
export default (state: Todo[] = [], action: TodosAction) => {
```

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.

```ts
switch (action.type) {
case getType(add):
// below action type is narrowed to: { type: "todos/ADD"; payload: Todo; }
return [...state, action.payload];
...
```

[⇧ back to top](#table-of-contents)

### Async-Flows

#### With `redux-observable` epics

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.

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`.
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.

```ts
// actions.ts
import { createAsyncAction } from 'typesafe-actions';

const fetchTodosAsync = createAsyncAction(
'FETCH_TODOS_REQUEST',
'FETCH_TODOS_SUCCESS',
'FETCH_TODOS_FAILURE',
'FETCH_TODOS_CANCEL'
)();

// epics.ts
import { fetchTodosAsync } from './actions';

const fetchTodosFlow: Epic = (action$, state$, { todosApi }) =>
action$.pipe(
filter(isActionOf(fetchTodosAsync.request)),
switchMap(action =>
from(todosApi.getAll(action.payload)).pipe(
map(fetchTodosAsync.success),
catchError((message: string) => of(fetchTodosAsync.failure(message))),
takeUntil(action$.pipe(filter(isActionOf(fetchTodosAsync.cancel)))),
)
)
);
```

#### With `redux-saga` sagas
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.

Typescript issues:
- [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(...`

*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.*

```ts
import { createAsyncAction, createReducer } from 'typesafe-actions';
import { put, call, takeEvery, all } from 'redux-saga/effects';

// Create the set of async actions
const fetchTodosAsync = createAsyncAction(
'FETCH_TODOS_REQUEST',
'FETCH_TODOS_SUCCESS',
'FETCH_TODOS_FAILURE'
)();

// Handle request saga
function* addTodoSaga(action: ReturnType): Generator {
try {
const response: Todo[] = yield call(todosApi.getAll, action.payload);

yield put(fetchTodosAsync.success(response));
} catch (err) {
yield put(fetchTodosAsync.failure(err));
}
}

// Main saga
function* mainSaga() {
yield all([
takeEvery(fetchTodosAsync.request, addTodoSaga),
]);
}

// Handle success reducer
export const todoReducer = createReducer({})
.handleAction(fetchTodosAsync.success, (state, action) => ({ ...state, todos: action.payload }));
```

[⇧ back to top](#table-of-contents)

---

## API Docs v4 (v5 is WIP [#189](https://github.com/piotrwitek/typesafe-actions/issues/189))

### Action-Creators API

#### `action`

_Simple **action factory function** to simplify creation of type-safe actions._

> **WARNING**:
> 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**.

```ts
action(type, payload?, meta?, error?)
```

Examples:
[> Advanced Usage Examples](src/action.spec.ts)

```ts
const increment = () => action('INCREMENT');
// { type: 'INCREMENT'; }

const createUser = (id: number, name: string) =>
action('CREATE_USER', { id, name });
// { type: 'CREATE_USER'; payload: { id: number; name: string }; }

const getUsers = (params?: string) =>
action('GET_USERS', undefined, params);
// { type: 'GET_USERS'; meta: string | undefined; }
```

> **TIP**: Starting from TypeScript v3.4 you can achieve similar results using new `as const` operator.

```ts
const increment = () => ({ type: 'INCREMENT' } as const);
```

#### `createAction`

_Create an enhanced action-creator with unlimited number of arguments._
- Resulting action-creator will preserve semantic names of their arguments `(id, title, amount, etc...)`.
- Returned action object have predefined properties `({ type, payload, meta })`

```ts
createAction(type)
createAction(type, actionCallback => {
return (namedArg1, namedArg2, ...namedArgN) => actionCallback(payload?, meta?)
})
```
> **TIP**: Injected `actionCallback` argument is similar to `action` API but doesn't need the "type" parameter

Examples:
[> Advanced Usage Examples](src/create-action.spec.ts)

```ts
import { createAction } from 'typesafe-actions';

// - with type only
const increment = createAction('INCREMENT');
dispatch(increment());
// { type: 'INCREMENT' };

// - with type and payload
const add = createAction('ADD', action => {
return (amount: number) => action(amount);
});
dispatch(add(10));
// { type: 'ADD', payload: number }

// - with type and meta
const getTodos = createAction('GET_TODOS', action => {
return (params: Params) => action(undefined, params);
});
dispatch(getTodos('some_meta'));
// { type: 'GET_TODOS', meta: Params }

// - and finally with type, payload and meta
const getTodo = createAction('GET_TODO', action => {
return (id: string, meta: string) => action(id, meta);
});
dispatch(getTodo('some_id', 'some_meta'));
// { type: 'GET_TODO', payload: string, meta: string }
```

[⇧ back to top](#table-of-contents)

#### `createStandardAction`

_Create an enhanced action-creator compatible with [Flux Standard Action](https://github.com/redux-utilities/flux-standard-action) to reduce boilerplate and enforce convention._
- Resulting action-creator have predefined arguments `(payload, meta)`
- Returned action object have predefined properties `({ type, payload, meta, error })`
- But it also contains a `.map()` method that allow to map `(payload, meta)` arguments to a custom action object `({ customProp1, customProp2, ...customPropN })`

```ts
createStandardAction(type)()
createStandardAction(type)()
createStandardAction(type).map((payload, meta) => ({ customProp1, customProp2, ...customPropN }))
```

> **TIP**: Using `undefined` as generic type parameter you can make the action-creator function require NO parameters.

Examples:
[> Advanced Usage Examples](src/create-standard-action.spec.ts)

```ts
import { createStandardAction } from 'typesafe-actions';

// Very concise with use of generic type arguments
// - with type only
const increment = createStandardAction('INCREMENT')();
const increment = createStandardAction('INCREMENT')();
increment(); // { type: 'INCREMENT' } (no parameters are required)

// - with type and payload
const add = createStandardAction('ADD')();
add(10); // { type: 'ADD', payload: number }

// - with type and meta
const getData = createStandardAction('GET_DATA')();
getData(undefined, 'meta'); // { type: 'GET_DATA', meta: string }

// - and finally with type, payload and meta
const getData = createStandardAction('GET_DATA')();
getData(1, 'meta'); // { type: 'GET_DATA', payload: number, meta: string }

// Can map payload and meta arguments to a custom action object
const notify = createStandardAction('NOTIFY').map(
(payload: string, meta: Meta) => ({
from: meta.username,
message: `${meta.username}: ${payload}`,
messageType: meta.type,
datetime: new Date(),
})
);

dispatch(notify('Hello!', { username: 'Piotr', type: 'announcement' }));
// { type: 'NOTIFY', from: string, message: string, messageType: MessageType, datetime: Date }
```

[⇧ back to top](#table-of-contents)

#### `createCustomAction`

_Create an enhanced action-creator with unlimited number of arguments and custom properties on action object._
- Resulting action-creator will preserve semantic names of their arguments `(id, title, amount, etc...)`.
- Returned action object have custom properties `({ type, customProp1, customProp2, ...customPropN })`

```ts
createCustomAction(type, type => {
return (namedArg1, namedArg2, ...namedArgN) => ({ type, customProp1, customProp2, ...customPropN })
})
```

Examples:
[> Advanced Usage Examples](src/create-custom-action.spec.ts)

```ts
import { createCustomAction } from 'typesafe-actions';

const add = createCustomAction('CUSTOM', type => {
return (first: number, second: number) => ({ type, customProp1: first, customProp2: second });
});

dispatch(add(1));
// { type: "CUSTOM"; customProp1: number; customProp2: number; }
```

[⇧ back to top](#table-of-contents)

#### `createAsyncAction`

_Create an object containing three enhanced action-creators to simplify handling of async flows (e.g. network request - request/success/failure)._

```ts
createAsyncAction(
requestType, successType, failureType, cancelType?
)()
```

##### `AsyncActionCreator`

```ts
type AsyncActionCreator<
[TRequestType, TRequestPayload],
[TSuccessType, TSuccessPayload],
[TFailureType, TFailurePayload],
[TCancelType, TCancelPayload]?
> = {
request: StandardActionCreator,
success: StandardActionCreator,
failure: StandardActionCreator,
cancel?: StandardActionCreator,
}
```

> **TIP**: Using `undefined` as generic type parameter you can make the action-creator function require NO parameters.

Examples:
[> Advanced Usage Examples](src/create-async-action.spec.ts)

```ts
import { createAsyncAction, AsyncActionCreator } from 'typesafe-actions';

const fetchUsersAsync = createAsyncAction(
'FETCH_USERS_REQUEST',
'FETCH_USERS_SUCCESS',
'FETCH_USERS_FAILURE'
)();

dispatch(fetchUsersAsync.request(params));

dispatch(fetchUsersAsync.success(response));

dispatch(fetchUsersAsync.failure(err));

const fn = (
a: AsyncActionCreator<
['FETCH_USERS_REQUEST', string],
['FETCH_USERS_SUCCESS', User[]],
['FETCH_USERS_FAILURE', Error]
>
) => a;
fn(fetchUsersAsync);

// There is 4th optional argument to declare cancel action
const fetchUsersAsync = createAsyncAction(
'FETCH_USERS_REQUEST',
'FETCH_USERS_SUCCESS',
'FETCH_USERS_FAILURE'
'FETCH_USERS_CANCEL'
)();

dispatch(fetchUsersAsync.cancel('reason'));

const fn = (
a: AsyncActionCreator<
['FETCH_USERS_REQUEST', string],
['FETCH_USERS_SUCCESS', User[]],
['FETCH_USERS_FAILURE', Error],
['FETCH_USERS_CANCEL', string]
>
) => a;
fn(fetchUsersAsync);
```

[⇧ back to top](#table-of-contents)

---

### Reducer-Creators API

#### `createReducer`

_Create a typesafe reducer_

```ts
createReducer(initialState, handlersMap?)
// or
createReducer(initialState)
.handleAction(actionCreator, reducer)
.handleAction([actionCreator1, actionCreator2, ...actionCreatorN], reducer)
.handleType(type, reducer)
.handleType([type1, type2, ...typeN], reducer)
```

Examples:
[> Advanced Usage Examples](src/create-reducer.spec.ts)

> **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
```ts
// type-free syntax doesn't require generic type arguments
const counterReducer = createReducer(0, {
ADD: (state, action) => state + action.payload,
[getType(increment)]: (state, _) => state + 1,
})
```

**Object map style:**
```ts
import { createReducer, getType } from 'typesafe-actions'

type State = number;
type Action = { type: 'ADD', payload: number } | { type: 'INCREMENT' };

const counterReducer = createReducer(0, {
ADD: (state, action) => state + action.payload,
[getType(increment)]: (state, _) => state + 1,
})
```

**Chain API style:**
```ts
// using action-creators
const counterReducer = createReducer(0)
.handleAction(add, (state, action) => state + action.payload)
.handleAction(increment, (state, _) => state + 1)

// handle multiple actions by using array
.handleAction([add, increment], (state, action) =>
state + (action.type === 'ADD' ? action.payload : 1)
);

// all the same scenarios are working when using type-constants
const counterReducer = createReducer(0)
.handleType('ADD', (state, action) => state + action.payload)
.handleType('INCREMENT', (state, _) => state + 1);
```

**Extend or compose reducers - every operation is completely typesafe:**
```ts
const newCounterReducer = createReducer(0)
.handleAction('SUBTRACT', (state, action) => state - action.payload)
.handleAction('DECREMENT', (state, _) => state - 1);

const bigReducer = createReducer(0, {
...counterReducer.handlers, // typesafe
...newCounterReducer.handlers, // typesafe
SUBTRACT: decrementReducer.handlers.DECREMENT, // <= error, wrong type
})
```

[⇧ back to top](#table-of-contents)

---

### Action-Helpers API

#### `getType`

_Get the **type** property value (narrowed to literal type) of given enhanced action-creator._

```ts
getType(actionCreator)
```

[> Advanced Usage Examples](src/get-type.spec.ts)

Examples:
```ts
import { getType, createStandardAction } from 'typesafe-actions';

const add = createStandardAction('ADD')();

// In switch reducer
switch (action.type) {
case getType(add):
// action type is { type: "ADD"; payload: number; }
return state + action.payload;

default:
return state;
}

// or with conditional statements
if (action.type === getType(add)) {
// action type is { type: "ADD"; payload: number; }
}
```

[⇧ back to top](#table-of-contents)

#### `isActionOf`

_Check if action is an instance of given enhanced action-creator(s)
(it will narrow action type to a type of given action-creator(s))_

> **WARNING**: Regular action creators and [action](#action) will not work with this helper

```ts
// can be used as a binary function
isActionOf(actionCreator, action)
// or as a curried function
isActionOf(actionCreator)(action)
// also accepts an array
isActionOf([actionCreator1, actionCreator2, ...actionCreatorN], action)
// with its curried equivalent
isActionOf([actionCreator1, actionCreator2, ...actionCreatorN])(action)
```

Examples:
[> Advanced Usage Examples](src/is-action-of.spec.ts)

```ts
import { addTodo, removeTodo } from './todos-actions';

// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.)
// - single action
[action1, action2, ...actionN]
.filter(isActionOf(addTodo)) // only actions with type `ADD` will pass
.map((action) => {
// action type is { type: "todos/ADD"; payload: Todo; }
...

// - multiple actions
[action1, action2, ...actionN]
.filter(isActionOf([addTodo, removeTodo])) // only actions with type `ADD` or 'REMOVE' will pass
.do((action) => {
// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
...

// With conditional statements
// - single action
if(isActionOf(addTodo, action)) {
return iAcceptOnlyTodoType(action.payload);
// action type is { type: "todos/ADD"; payload: Todo; }
}
// - multiple actions
if(isActionOf([addTodo, removeTodo], action)) {
return iAcceptOnlyTodoType(action.payload);
// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
}
```

[⇧ back to top](#table-of-contents)

#### `isOfType`

_Check if action type property is equal given type-constant(s)
(it will narrow action type to a type of given action-creator(s))_

```ts
// can be used as a binary function
isOfType(type, action)
// or as curried function
isOfType(type)(action)
// also accepts an array
isOfType([type1, type2, ...typeN], action)
// with its curried equivalent
isOfType([type1, type2, ...typeN])(action)
```

Examples:
[> Advanced Usage Examples](src/is-of-type.spec.ts)

```ts
import { ADD, REMOVE } from './todos-types';

// Works with any filter type function (`Array.prototype.filter`, lodash, ramda, rxjs, etc.)
// - single action
[action1, action2, ...actionN]
.filter(isOfType(ADD)) // only actions with type `ADD` will pass
.map((action) => {
// action type is { type: "todos/ADD"; payload: Todo; }
...

// - multiple actions
[action1, action2, ...actionN]
.filter(isOfType([ADD, REMOVE])) // only actions with type `ADD` or 'REMOVE' will pass
.do((action) => {
// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
...

// With conditional statements
// - single action
if(isOfType(ADD, action)) {
return iAcceptOnlyTodoType(action.payload);
// action type is { type: "todos/ADD"; payload: Todo; }
}
// - multiple actions
if(isOfType([ADD, REMOVE], action)) {
return iAcceptOnlyTodoType(action.payload);
// action type is { type: "todos/ADD"; payload: Todo; } | { type: "todos/REMOVE"; payload: Todo; }
}
```

[⇧ back to top](#table-of-contents)

---

### Type-Helpers API
Below helper functions are very flexible generalizations, works great with nested structures and will cover numerous different use-cases.

#### `ActionType`

_Powerful type-helper that will infer union type from **import * as ...** or **action-creator map** object._

```ts
import { ActionType } from 'typesafe-actions';

// with "import * as ..."
import * as todos from './actions';
export type TodosAction = ActionType;
// TodosAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }

// with nested action-creator map case
const actions = {
action1: createAction('action1'),
nested: {
action2: createAction('action2'),
moreNested: {
action3: createAction('action3'),
},
},
};
export type RootAction = ActionType;
// RootAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }
```

[⇧ back to top](#table-of-contents)

#### `StateType`

_Powerful type helper that will infer state object type from **reducer function** and **nested/combined reducers**._

> **WARNING**: working with redux@4+ types

```ts
import { combineReducers } from 'redux';
import { StateType } from 'typesafe-actions';

// with reducer function
const todosReducer = (state: Todo[] = [], action: TodosAction) => {
switch (action.type) {
case getType(todos.add):
return [...state, action.payload];
...
export type TodosState = StateType;

// with nested/combined reducers
const rootReducer = combineReducers({
router: routerReducer,
counters: countersReducer,
});
export type RootState = StateType;
```

[⇧ back to top](#table-of-contents)

---

## Migration Guides

### `v4.x.x` to `v5.x.x`

**Breaking changes:**

1. In `v5` all the deprecated `v4` creator functions are available under `deprecated` named import to help with incremental migration.
```ts
// before
import { createAction, createStandardAction, createCustomAction } from "typesafe-actions"

// after
import { deprecated } from "typesafe-actions"
const { createAction, createStandardAction, createCustomAction } = deprecated;
```

2. `createStandardAction` was renamed to `createAction` and `.map` method was removed in favor of simpler `redux-actions` style API.
```ts
// before
const withMappedPayloadAndMeta = createStandardAction(
'CREATE_STANDARD_ACTION'
).map(({ username, message }: Notification) => ({
payload: `${username}: ${message}`,
meta: { username, message },
}));

// after
const withMappedPayloadAndMeta = createAction(
'CREATE_STANDARD_ACTION',
({ username, message }: Notification) => `${username}: ${message}`, // payload creator
({ username, message }: Notification) => ({ username, message }) // meta creator
)();
```

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.
```ts
// before
const withPayloadAndMeta = createAction('CREATE_ACTION', resolve => {
return (id: number, token: string) => resolve(id, token);
});

// after
const withPayloadAndMeta = createAction(
'CREATE_ACTION',
(id: number, token: string) => id, // payload creator
(id: number, token: string) => token // meta creator
})();
```

4. `createCustomAction` - API was greatly simplified, now it's used like this:
```ts
// before
const add = createCustomAction('CUSTOM', type => {
return (first: number, second: number) => ({ type, customProp1: first, customProp2: second });
});

// after
const add = createCustomAction(
'CUSTOM',
(first: number, second: number) => ({ customProp1: first, customProp2: second })
);
```

5. `AsyncActionCreator` should be just renamed to `AsyncActionCreatorBuilder`.
```ts
// before
import { AsyncActionCreator } from "typesafe-actions"

//after
import { AsyncActionCreatorBuilder } from "typesafe-actions"
```

### `v3.x.x` to `v4.x.x`

**No breaking changes!**

### `v2.x.x` to `v3.x.x`

Minimal supported TypeScript `v3.1+`.

### `v1.x.x` to `v2.x.x`

**Breaking changes:**

1. `createAction`
- In `v2` we provide a `createActionDeprecated` function compatible with `v1` `createAction` to help with incremental migration.

```ts
// in v1 we created action-creator like this:
const getTodo = createAction('GET_TODO',
(id: string, meta: string) => ({
type: 'GET_TODO',
payload: id,
meta: meta,
})
);

getTodo('some_id', 'some_meta'); // { type: 'GET_TODO', payload: 'some_id', meta: 'some_meta' }

// in v2 we offer few different options - please choose your preference
const getTodoNoHelpers = (id: string, meta: string) => action('GET_TODO', id, meta);

const getTodoWithHelpers = createAction('GET_TODO', action => {
return (id: string, meta: string) => action(id, meta);
});

const getTodoFSA = createStandardAction('GET_TODO')();

const getTodoCustom = createStandardAction('GET_TODO').map(
({ id, meta }: { id: string; meta: string; }) => ({
payload: id,
meta,
})
);
```

[⇧ back to top](#table-of-contents)

### Migrating from `redux-actions` to `typesafe-actions`

- createAction(s)

```ts
createAction(type, payloadCreator, metaCreator) => createStandardAction(type)() || createStandardAction(type).map(payloadMetaCreator)

createActions() => // COMING SOON!
```

- handleAction(s)

```ts
handleAction(type, reducer, initialState) => createReducer(initialState).handleAction(type, reducer)

handleActions(reducerMap, initialState) => createReducer(initialState, reducerMap)
```

> 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.

- combineActions

Not needed because each function in the API accept single value or array of values for action types or action creators.

[⇧ back to top](#table-of-contents)

---

## Compatibility Notes

**TypeScript support**

- `5.X.X` - TypeScript v3.2+
- `4.X.X` - TypeScript v3.2+
- `3.X.X` - TypeScript v3.2+
- `2.X.X` - TypeScript v2.9+
- `1.X.X` - TypeScript v2.7+

**Browser support**

It's compatible with all modern browsers.

For older browsers support (e.g. IE <= 11) and some mobile devices you need to provide the following polyfills:
- [Object.assign](https://developer.mozilla.org/pl/docs/Web/JavaScript/Referencje/Obiekty/Object/assign#Polyfill)
- [Array.prototype.includes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes)

**Recommended polyfill for IE**

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`.
Please check the `React` guidelines on how to do that: [LINK](https://reactjs.org/docs/javascript-environment-requirements.html)
A polyfill fo IE11 is included in our `/codesandbox` application.

[⇧ back to top](#table-of-contents)

---

## Recipes

### Restrict Meta type in `action` creator
Using this recipe you can create an action creator with restricted Meta type with exact object shape.

```tsx
export type MetaType = {
analytics?: {
eventName: string;
};
};

export const actionWithRestrictedMeta = (
type: T,
payload: P,
meta: MetaType
) => action(type, payload, meta);

export const validAction = (payload: string) =>
actionWithRestrictedMeta('type', payload, { analytics: { eventName: 'success' } }); // OK!

export const invalidAction = (payload: string) =>
actionWithRestrictedMeta('type', payload, { analytics: { excessProp: 'no way!' } }); // Error
// Object literal may only specify known properties, and 'excessProp' does not exist in type '{ eventName: string; }
```

[⇧ back to top](#table-of-contents)

---

## Compare to others

Here you can find out a detailed comparison of `typesafe-actions` to other solutions.

### `redux-actions`
Lets compare the 3 most common variants of action-creators (with type only, with payload and with payload + meta)

Note: tested with "@types/redux-actions": "2.2.3"

**- with type only (no payload)**

##### redux-actions
```ts
const notify1 = createAction('NOTIFY');
// resulting type:
// () => {
// type: string;
// payload: void | undefined;
// error: boolean | undefined;
// }
```

> 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)

##### typesafe-actions
```ts
const notify1 = () => action('NOTIFY');
// resulting type:
// () => {
// type: "NOTIFY";
// }
```

> with `typesafe-actions` there is no excess nullable types and no excess properties and the action "type" property is containing a literal type

**- with payload**

##### redux-actions
```ts
const notify2 = createAction('NOTIFY',
(username: string, message?: string) => ({
message: `${username}: ${message || 'Empty!'}`,
})
);
// resulting type:
// (t1: string) => {
// type: string;
// payload: { message: string; } | undefined;
// error: boolean | undefined;
// }
```

> 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

##### typesafe-actions
```ts
const notify2 = (username: string, message?: string) => action(
'NOTIFY',
{ message: `${username}: ${message || 'Empty!'}` },
);
// resulting type:
// (username: string, message?: string | undefined) => {
// type: "NOTIFY";
// payload: { message: string; };
// }
```

> `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

**- with payload and meta**

##### redux-actions
```ts
const notify3 = createAction('NOTIFY',
(username: string, message?: string) => (
{ message: `${username}: ${message || 'Empty!'}` }
),
(username: string, message?: string) => (
{ username, message }
)
);
// resulting type:
// (...args: any[]) => {
// type: string;
// payload: { message: string; } | undefined;
// meta: { username: string; message: string | undefined; };
// error: boolean | undefined;
// }
```

> this time we got a completely broken arguments arity with no type-safety because of `any` type with all the earlier issues

##### typesafe-actions
```ts
/**
* typesafe-actions
*/
const notify3 = (username: string, message?: string) => action(
'NOTIFY',
{ message: `${username}: ${message || 'Empty!'}` },
{ username, message },
);
// resulting type:
// (username: string, message?: string | undefined) => {
// type: "NOTIFY";
// payload: { message: string; };
// meta: { username: string; message: string | undefined; };
// }
```

> `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

[⇧ back to top](#table-of-contents)

---

## Motivation

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).

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 😱)

**So I created `typesafe-actions` to address all of the above pain points.**

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 will create additional noise in your code.

[⇧ back to top](#table-of-contents)

---

## Contributing

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)

[⇧ back to top](#table-of-contents)

---

## Funding Issues

You can also help by funding issues.
Issues like bug fixes or feature requests can be very quickly resolved when funded through the IssueHunt platform.

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.

[![Let's fund issues in this repository](https://issuehunt.io/static/embed/issuehunt-button-v1.svg)](https://issuehunt.io/repos/110746954)

[⇧ back to top](#table-of-contents)

---

## License

[MIT License](/LICENSE)

Copyright (c) 2017 Piotr Witek (http://piotrwitek.github.io)