typesafe-actions-zh



# typesafe-actions

> 主要是学习翻译用的,目前翻译不完整

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



This library is part of the React & Redux TypeScript Guide ecosystem :book:

Latest Stable Version
NPM Downloads
NPM Downloads
Bundlephobia Size

Build Status
Dependencies Status
License
Join the community on Spectrum



:star: Found it useful? Want more updates? Show your support by giving a :star:

:tada: Now updated to support TypeScript v3.5 :tada:


Buy Me a Coffee


Become a Patron



Features

  • minimalistic - according to size-snapshot (Minified: 3.48 KB, Gzipped: 1.03 KB), check also on bundlephobia
  • secure and optimized - no external dependencies, bundled in 3 different formats (cjs, esm and umd for browser) with separate optimized bundles for dev & prod (same as react)
  • focus on 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
  • focus on 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

Codesandbox links

  • Reference Todo-App implementation using typesafe-actions: Link
  • Starter to help reproduce bug reports: Link

Table of Contents


Motivation

当时我准备把Redux和Typescript进行结合开发,我尝试使用redux-actions 来减少需要维护的代码和 action-creators 模板。不幸的是,由于错误的类型以及不完整的类型接口布遍了代码,所以结果并不理想(点击这里查看对照)

现存的解决方案不是太冗余就是使用 class 来解决(可读性差,而且需要new关键字😱)

所以我创建了typesafe-actions 来解决上述的痛点

核心设计方案是设计一个使用TypeScript中type-inference💪力量的API来举起“可维护的负担”的类型注释。另外,我希望使他“看起来或者感觉起来”都尽可能的接近我们所习惯的JavaScript❤️,所以我不想写太多多余的注解来给代码带来过多的噪点。

⇧ back to top


安装

1
2
3
4
5
// NPM
npm install typesafe-actions

// YARN
yarn add typesafe-actions

⇧ back to top


兼容性注意

TypeScript support

浏览器支持情况

兼容所有主流浏览器。

旧的浏览器的支持情况:IE <= 11 ,某些手机浏览器你需要提供下列的扩展库

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 to learn how to do that: LINK
A polyfill fo IE11 is included in our /codesandbox application.

⇧ back to top


Contributing Guide

We are open for contributions. If you’re planning to contribute please make sure to read the contributing guide: CONTRIBUTING.md

⇧ back to top


Funding

Typesafe-Actions is an independent open-source project created by people investing their free time for the benefit of our community.

If you are using Typesafe-Actions please consider donating as this will guarantee the project will be updated and maintained in the long run.

Issues can be funded by anyone interested in them being resolved. Reward will be transparently distributed to the contributor handling the task through the IssueHunt platform.

Let's fund issues in this repository

⇧ back to top


Tutorial

为了展示使用了这个库type-safety后的灵活性和强大,让我们使用Redux结构来构建一个最常见的应用——经典的 todo-app 应用

注意
请确保你以具备以下的知识点以便能顺利的完成教程: Type Inference, Control flow analysis, Tagged union types, Generics and Advanced Types.

⇧ back to top

常量

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.

使用字符串常量时的Typescript局限 - 当使用字符创常量作为action的type时, 请确保使用简单易懂的字符串常量. 限制来自原 type-system, 因为所有的动态字符创操作(比如:字符串连接,字符串模板以及作为映射的对象)会扩大这个类型成为着急类,String。结果就是破坏了action的上下文意思。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Example file: './constants.ts'

// WARNING: 错误的使用方法
export const ADD = prefix + 'ADD'; // => string
export const ADD = `${prefix}/ADD`; // => string
export default {
ADD: '@prefix/ADD', // => string
}

// 正确的使用方法
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

Actions

不同的项目有不同的需求,不同的团队有不同的约定,这就是为什么我认为typesafe-actions设计的为什么需要设计的灵活。这里提供三种常用的不同方式让你选择,所以你可以选择最适合你团队的。

1. Basic actions

actioncreateAction 是两个使用预设好的属性 ({ type, payload, meta })来创建 actions的创建者. 这使他们简洁而又自以为是。

重要的属性是生成的action-creator将具有可变数量的参数并保留其语义名称(id,title,amount等)。

这两个创建者是非常像的,唯一不同的是 action 不能和 action-helpers一起使用。

1
2
3
4
5
6
7
8
9
10
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, 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.

1
2
3
4
5
6
7
8
9
10
11
import { createStandardAction } from 'typesafe-actions';

export const toggle = createStandardAction('todos/TOGGLE')<string>();
// 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.

1
2
3
4
5
6
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.

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.

1
2
3
4
5
6
7
8
9
10
// 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

Action Helpers

现在我想展时以下action-helpers以及解释一下他的用例。我们将要实现一个没有副作用的当用户成功添加新数据时展现成功的提示。

主要注意以下,所有的这些helpers都是扮演者类型守护者,所以他们全部都是小标签组成的唯一Type(RootAction)来指定我们想要的action类型

使用action-creators来替代类型常量

我们可以用action-creators来替代类型常量来匹配reduces里指定的action以及epics用例。通过为包含类型字符的action-creator实例添加静态属性来使用

最常用的命令就是getType,他常用于常规的reducer中的switch cases:

1
2
3
4
5
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).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// epics.ts
import { isActionOf } from 'typesafe-actions';

import { add } from './actions';

const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (action$, store, { 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// epics.ts
import { isOfType } from 'typesafe-actions';

import { ADD } from './constants';

const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (action$, store, { 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.

1
2
3
4
5
6
7
8
9
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

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// types.d.ts
import { StateType, ActionType } from 'typesafe-actions';

export type RootAction = ActionType<typeof import('./actions').default>;

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

// now you can use
createReducer(...)
// instead of
createReducer<State, Action>(...)

Using createReducer API with type-free syntax

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

Using handleAction chain API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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.

1
2
3
4
import { ActionType } from 'typesafe-actions';

import * as todos from './actions';
export type TodosAction = ActionType<typeof todos>;

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

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

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

⇧ back to top

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// actions.ts
import { createAsyncAction } from 'typesafe-actions';

const fetchTodosAsync = createAsyncAction(
'FETCH_TODOS_REQUEST',
'FETCH_TODOS_SUCCESS',
'FETCH_TODOS_FAILURE',
'FETCH_TODOS_CANCEL'
)<string, Todo[], Error, string>();

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

const fetchTodosFlow: Epic<RootAction, RootAction, RootState, Services> = (action$, store, { todosApi }) =>
action$.pipe(
filter(isActionOf(fetchTodosAsync.request)),
switchMap(action =>
from(todosApi.getAll(action.payload)).pipe(
map(fetchTodosAsync.success),
catchError(pipe(fetchTodosAsync.failure, of)),
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:

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createAsyncAction } from 'typesafe-actions';

const fetchTodosAsync = createAsyncAction(
'FETCH_TODOS_REQUEST',
'FETCH_TODOS_SUCCESS',
'FETCH_TODOS_FAILURE'
)<string, Todo[], Error>();

function* addTodoSaga(action: ReturnType<typeof fetchTodosAsync.request>): Generator {
const response: Todo[] = yield call(todosApi.getAll, action.payload);

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

⇧ back to top


API Docs

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.

1
action(type, payload?, meta?, error?)

Examples:
> Advanced Usage Examples

1
2
3
4
5
6
7
8
9
10
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.

1
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 })
1
2
3
4
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

createStandardAction

Create an enhanced action-creator compatible with 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 })
1
2
3
createStandardAction(type)()
createStandardAction(type)<TPayload, TMeta?>()
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { createStandardAction } from 'typesafe-actions';

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


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

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

// - and finally with type, payload and meta
const getData = createStandardAction('GET_DATA')<number, string>();
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: `${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

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 })
1
2
3
createCustomAction(type, type => {
return (namedArg1, namedArg2, ...namedArgN) => ({ type, customProp1, customProp2, ...customPropN })
})

Examples:
> Advanced Usage Examples

1
2
3
4
5
6
7
8
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

createAsyncAction

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

1
2
3
createAsyncAction(
requestType, successType, failureType, cancelType?
)<TRequestPayload, TSuccessPayload, TFailurePayload, TCancelPayload?>()
AsyncActionCreator
1
2
3
4
5
6
7
8
9
10
11
type AsyncActionCreator<
[TRequestType, TRequestPayload],
[TSuccessType, TSuccessPayload],
[TFailureType, TFailurePayload],
[TCancelType, TCancelPayload]?
> = {
request: StandardActionCreator<TRequestType, TRequestPayload>,
success: StandardActionCreator<TSuccessType, TSuccessPayload>,
failure: StandardActionCreator<TFailureType, TFailurePayload>,
cancel?: StandardActionCreator<TCancelType, TCancelPayload>,
}

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

Examples:
> Advanced Usage Examples

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { createAsyncAction, AsyncActionCreator } from 'typesafe-actions';

const fetchUsersAsync = createAsyncAction(
'FETCH_USERS_REQUEST',
'FETCH_USERS_SUCCESS',
'FETCH_USERS_FAILURE'
)<string, User[], Error>();

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'
)<string, User[], Error, string>();

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


Reducer-Creators API

createReducer

Create a typesafe reducer

1
2
3
4
5
createReducer<TState, TRootAction>(initialState, handlersMap?)
.handleAction(type, reducer)
.handleAction([type1, type2, ...typeN], reducer)
.handleAction(actionCreator, reducer)
.handleAction([actionCreator1, actionCreator2, ...actionCreatorN], reducer)

Examples:
> Advanced Usage Examples

TIP: You can use reducer API with a type-free syntax by Extending internal types, otherwise you’ll have to pass generic type arguments like in below examples

1
2
3
4
5
// type-free syntax doesn't require generic type arguments
const counterReducer = createReducer(0, {
ADD: (state, action) => state + action.payload,
[getType(increment)]: (state, _) => state + 1,
})

Using type-constants as keys in the object map:

1
2
3
4
5
6
7
8
9
10
11
12
import { createReducer, getType } from 'typesafe-actions'

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

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

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

Using handleAction chain API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// using action-creators
const counterReducer = createReducer<State, Action>(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<State, Action>(0)
.handleAction('ADD', (state, action) => state + action.payload)
.handleAction('INCREMENT', (state, _) => state + 1);

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

Extend or compose various reducers together - every operation is completely typesafe:

1
2
3
4
5
6
7
8
9
const newCounterReducer = createReducer<State, Action>(0)
.handleAction('SUBTRACT', (state, action) => state - action.payload)
.handleAction('DECREMENT', (state, _) => state - 1);

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

⇧ back to top


Action-Helpers API

getType

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

1
getType(actionCreator)

> Advanced Usage Examples

Examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { getType, createStandardAction } from 'typesafe-actions';

const add = createStandardAction('ADD')<number>();

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

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 will not work with this helper

1
2
3
4
5
6
7
8
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

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

1
2
3
4
5
6
7
8
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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


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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { ActionType } from 'typesafe-actions';

// with "import * as ..."
import * as todos from './actions';
export type TodosAction = ActionType<typeof todos>;
// 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<typeof actions>;
// RootAction: { type: 'action1' } | { type: 'action2' } | { type: 'action3' }

⇧ back to top

StateType

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

WARNING: working with redux@4+ types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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<typeof todosReducer>;

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

⇧ back to top


Migration Guides

v3.x.x to v4.x.x

From v4.x.x all action creators will use undefined instead of void as a generic type parameter to make the action-creator function require NO parameters.

1
2
3
4
5
6
7
8
9
const increment = createStandardAction('INCREMENT')<undefined>();
increment(); // <= no parameters required

const fetchUsersAsync = createAsyncAction(
'FETCH_USERS_REQUEST',
'FETCH_USERS_SUCCESS',
'FETCH_USERS_FAILURE'
)<undefined, User[], Error>();
fetchUsersAsync.request(); // <= no parameters required

v2.x.x to v3.x.x

v3.x.x API is backward compatible with v2.x.x. You’ll only need to update typescript dependency to > v3.1.

v1.x.x to v2.x.x

In v2 we provide a createActionDeprecated function compatible with v1 API to help with incremental migration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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 API we offer few different styles - 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')<string, string>();

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

Migrating from redux-actions to typesafe-actions

  • createAction(s)
1
2
3
createAction(type, payloadCreator, metaCreator) => createStandardAction(type)() || createStandardAction(type).map(payloadMetaCreator)

createActions() => // COMING SOON!
  • handleAction(s)
1
2
3
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


Recipes

Restrict Meta type in action creator

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export type MetaType = {
analytics?: {
eventName: string;
};
};

export const actionWithRestrictedMeta = <T extends string, P>(
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


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
1
2
3
4
5
6
7
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
1
2
3
4
5
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
1
2
3
4
5
6
7
8
9
10
11
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
1
2
3
4
5
6
7
8
9
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 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


MIT License

Copyright (c) 2017 Piotr Witek piotrek.witek@gmail.com (http://piotrwitek.github.io)