‚̧Neutered Paranoid Meerkat
    Wondering what‚Äôs next for npm?Check out our public roadmap! ¬Ľ

    typesafe-actions
    TypeScript icon, indicating that this package has built-in type declarations

    5.1.0¬†‚Äʬ†Public¬†‚Äʬ†Published

    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 ūüďĖ

    Latest Stable Version NPM Downloads NPM Downloads Bundlephobia Size

    Build Status Dependency Status License Join the community on Spectrum

    Found it useful? Want more updates?

    Show your support by giving a ‚≠źÔłŹ

    Buy Me a Coffee Become a Patron



    What's new?

    ūüéČ Now updated to support TypeScript v3.7 ūüéČ

    ‚ö†ÔłŹ Library was recently updated to v5 ‚ö†ÔłŹ
    Current API Docs and Tutorial are outdated (from v4), so temporarily please use this issue as v5.x.x API Docs.



    Features

    Playgrounds & Examples

    Goals

    • Secure and Minimal - no third-party dependencies, according to size-snapshot (Minified: 3.48 KB, Gzipped: 1.03 KB), check also on bundlephobia
    • 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

    # NPM 
    npm install typesafe-actions
     
    # YARN 
    yarn add typesafe-actions

    ‚áß back to top


    Tutorial v4 (v5 is WIP #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, Control flow analysis, Tagged union types, Generics and Advanced Types.

    ‚áß back to top

    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.

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

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

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

    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.

    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.

    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.

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

    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:

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

    // epics.ts
    import { isActionOf } from 'typesafe-actions';
     
    import { add } from './actions';
     
    const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (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.

    // epics.ts
    import { isOfType } from 'typesafe-actions';
     
    import { ADD } from './constants';
     
    const addTodoToast: Epic<RootAction, RootAction, RootState, Services> = (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.

    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:

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

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

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

    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.

      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.

    // 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$, 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:

    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.

    import { createAsyncAction, createReducer } from 'typesafe-actions';
    import { put, call, takeEvery } from 'redux-saga/effetcs';
     
    // Create the set of async actions
    const fetchTodosAsync = createAsyncAction(
      'FETCH_TODOS_REQUEST',
      'FETCH_TODOS_SUCCESS',
      'FETCH_TODOS_FAILURE'
    )<string, Todo[], Error>();
     
    // Handle request saga
    function* addTodoSaga(action: ReturnType<typeof fetchTodosAsync.request>): 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


    API Docs v4 (v5 is WIP #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.

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

    Examples: > Advanced Usage Examples

    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.

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

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

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

    Examples: > Advanced Usage Examples

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

    createAsyncAction(
      requestType, successType, failureType, cancelType?
    )<TRequestPayload, TSuccessPayload, TFailurePayload, TCancelPayload?>()
    AsyncActionCreator
    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

    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

    createReducer<TState, TRootAction>(initialState, handlersMap?)
    // or
    createReducer<TState, TRootAction>(initialState)
      .handleAction(actionCreator, reducer)
      .handleAction([actionCreator1, actionCreator2, ...actionCreatorN], reducer)
      .handleType(type, reducer)
      .handleType([type1, type2, ...typeN], 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

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

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

    Chain API style:

    // 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)
      .handleType('ADD', (state, action) => state + action.payload)
      .handleType('INCREMENT', (state, _) => state + 1);

    Extend or compose reducers - every operation is completely typesafe:

    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.

    getType(actionCreator)

    > Advanced Usage Examples

    Examples:

    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

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

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

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

    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.

    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

    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

    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.
    // before
    import { createAction, createStandardAction, createCustomAction } from "typesafe-actions"
     
    // after
    import { deprecated } from "typesafe-actions"
    const { createAction, createStandardAction, createCustomAction } = deprecated;
    1. createStandardAction was renamed to createAction and .map method was removed in favor of simpler redux-actions style API.
    // 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
    )();
    1. 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.
    // 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
    })();
    1. createCustomAction - API was greatly simplified, now it's used like this:
    // 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 })
    );
    1. AsyncActionCreator should be just renamed to AsyncActionCreatorBuilder.
    // 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.
    // 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')<string, string>();
     
    const getTodoCustom = createStandardAction('GET_TODO').map(
      ({ id, meta }: { id: string; meta: string; }) => ({
        payload: id,
        meta,
      })
    );

    ‚áß back to top

    Migrating from redux-actions to typesafe-actions

    • createAction(s)
    createAction(type, payloadCreator, metaCreator) => createStandardAction(type)() || createStandardAction(type).map(payloadMetaCreator)
     
    createActions() => // COMING SOON!
    • handleAction(s)
    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


    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:

    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


    Recipes

    Restrict Meta type in action creator

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

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


    Motivation

    When I started to combine Redux with TypeScript, I was trying to use redux-actions 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).

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

    ‚áß back to top


    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

    ‚áß back to top


    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

    ‚áß back to top


    License

    MIT License

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

    Install

    npm i typesafe-actions

    DownloadsWeekly Downloads

    107,246

    Version

    5.1.0

    License

    MIT

    Unpacked Size

    296 kB

    Total Files

    32

    Last publish

    Collaborators

    • avatar