CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-typesafe-actions

Typesafe Action Creators for Redux / Flux Architectures in TypeScript

Pending
Overview
Eval results
Files

action-helpers.mddocs/

Action Helpers

Utilities for type-safe action inspection and filtering, including type guards for discriminated union types and action creator type extraction for Redux/Flux architectures.

Capabilities

getType

Extracts the type literal from a given action creator, providing compile-time access to action type constants.

/**
 * Get the type literal of a given action creator
 * @param actionCreator - Action creator with type metadata
 * @returns Type constant from the action creator
 */
function getType<TType extends TypeConstant>(
  actionCreator: ActionCreator<TType> & ActionCreatorTypeMetadata<TType>
): TType;

Usage Examples:

import { createAction, getType } from "typesafe-actions";

const increment = createAction('INCREMENT')<number>();
const decrement = createAction('DECREMENT')();

// Extract type constants
const incrementType = getType(increment);
// Result: 'INCREMENT' (as literal type)

const decrementType = getType(decrement);
// Result: 'DECREMENT' (as literal type)

// Use in switch statements
function handleAction(action: Action) {
  switch (action.type) {
    case getType(increment):
      // TypeScript knows this is increment action
      console.log('Incrementing by:', action.payload);
      break;
    case getType(decrement):
      // TypeScript knows this is decrement action
      console.log('Decrementing');
      break;
  }
}

// Use for action type constants object
const ActionTypes = {
  INCREMENT: getType(increment),
  DECREMENT: getType(decrement),
} as const;

isOfType

Type guard function to check if action type equals given type constant, works with discriminated union types and supports both curried and direct usage.

/**
 * Curried type guard to check if action type equals given type constant
 * @param type - Action type constant or array of type constants
 * @returns Curried function that takes action and returns type predicate
 */
function isOfType<T extends string>(
  type: T | T[]
): <A extends { type: string }>(
  action: A
) => action is A extends { type: T } ? A : never;

/**
 * Direct type guard to check if action type equals given type constant
 * @param type - Action type constant or array of type constants
 * @param action - Action to check
 * @returns Type predicate indicating if action matches type
 */
function isOfType<T extends string, A extends { type: string }>(
  type: T | T[],
  action: A
): action is A extends { type: T } ? A : never;

Usage Examples:

import { isOfType, createAction } from "typesafe-actions";

const increment = createAction('INCREMENT')<number>();
const decrement = createAction('DECREMENT')();
const reset = createAction('RESET')();

type CounterAction = 
  | ReturnType<typeof increment>
  | ReturnType<typeof decrement>
  | ReturnType<typeof reset>;

// Curried usage - filter arrays
const actions: CounterAction[] = [
  increment(5),
  decrement(),
  increment(3),
  reset(),
];

const incrementActions = actions.filter(isOfType('INCREMENT'));
// Result: increment actions only, with proper typing

const counterActions = actions.filter(isOfType(['INCREMENT', 'DECREMENT']));
// Result: increment and decrement actions, with union typing

// Direct usage - type guards
function handleAction(action: CounterAction) {
  if (isOfType(action, 'INCREMENT')) {
    // TypeScript knows action.payload is number
    console.log('Incrementing by:', action.payload);
  } else if (isOfType(action, ['DECREMENT', 'RESET'])) {
    // TypeScript knows action is decrement or reset
    console.log('Decrementing or resetting');
  }
}

// Epic/middleware usage
const incrementEpic = (action$: Observable<CounterAction>) =>
  action$.pipe(
    filter(isOfType('INCREMENT')),
    // action is properly typed as increment action
    map(action => console.log('Increment payload:', action.payload))
  );

isActionOf

Type guard function to check if action is instance of given action creator(s), works with discriminated union types and supports both curried and direct usage.

/**
 * Curried type guard to check if action is instance of given action creator
 * @param actionCreator - Action creator to check against
 * @returns Curried function that takes action and returns type predicate
 */
function isActionOf<AC extends ActionCreator<{ type: string }>>(
  actionCreator: AC | AC[]
): (action: { type: string }) => action is ReturnType<AC>;

/**
 * Direct type guard to check if action is instance of given action creator
 * @param actionCreator - Action creator to check against  
 * @param action - Action to check
 * @returns Type predicate indicating if action was created by action creator
 */
function isActionOf<AC extends ActionCreator<{ type: string }>>(
  actionCreator: AC | AC[],
  action: { type: string }
): action is ReturnType<AC>;

Usage Examples:

import { isActionOf, createAction, createAsyncAction } from "typesafe-actions";

const increment = createAction('INCREMENT')<number>();
const decrement = createAction('DECREMENT')();
const reset = createAction('RESET')();

const fetchUser = createAsyncAction(
  'FETCH_USER_REQUEST',
  'FETCH_USER_SUCCESS',
  'FETCH_USER_FAILURE'
)<void, User, Error>();

type AppAction = 
  | ReturnType<typeof increment>
  | ReturnType<typeof decrement>
  | ReturnType<typeof reset>
  | ReturnType<typeof fetchUser.request>
  | ReturnType<typeof fetchUser.success>
  | ReturnType<typeof fetchUser.failure>;

// Curried usage - filter arrays
const actions: AppAction[] = [
  increment(5),
  fetchUser.request(),
  decrement(),
  fetchUser.success({ id: 1, name: 'Alice' }),
  reset(),
];

const incrementActions = actions.filter(isActionOf(increment));
// Result: increment actions only, properly typed

const fetchActions = actions.filter(isActionOf([
  fetchUser.request,
  fetchUser.success,
  fetchUser.failure
]));
// Result: all fetch-related actions, with union typing

// Direct usage - type guards
function handleAction(action: AppAction) {
  if (isActionOf(action, increment)) {
    // TypeScript knows action.payload is number
    console.log('Incrementing by:', action.payload);
  } else if (isActionOf(action, fetchUser.success)) {
    // TypeScript knows action.payload is User
    console.log('User fetched:', action.payload.name);
  } else if (isActionOf(action, [fetchUser.request, fetchUser.failure])) {
    // TypeScript knows action is request or failure
    console.log('Fetch request or failure');
  }
}

// Reducer usage
const userReducer = (state: UserState, action: AppAction): UserState => {
  if (isActionOf(action, fetchUser.success)) {
    return {
      ...state,
      user: action.payload, // properly typed as User
      loading: false,
    };
  }
  
  if (isActionOf(action, [fetchUser.request])) {
    return {
      ...state,
      loading: true,
      error: null,
    };
  }
  
  return state;
};

// Epic/middleware usage
const fetchUserEpic = (action$: Observable<AppAction>) =>
  action$.pipe(
    filter(isActionOf(fetchUser.request)),
    // action is properly typed as request action
    switchMap(() =>
      api.fetchUser().pipe(
        map(user => fetchUser.success(user)),
        catchError(error => of(fetchUser.failure(error)))
      )
    )
  );

Advanced Usage Patterns

Combining with Type Guards

import { isActionOf, isOfType, createAction } from "typesafe-actions";

const actions = [
  createAction('SET_LOADING')<boolean>(),
  createAction('SET_ERROR')<string>(),
  createAction('CLEAR_STATE')(),
];

// Combine multiple type guards
function isLoadingOrError(action: Action) {
  return isOfType(action, ['SET_LOADING', 'SET_ERROR']) ||
         isActionOf(action, actions.slice(0, 2));
}

// Use in complex filtering
const relevantActions = allActions.filter(action => 
  isActionOf(action, [actions[0], actions[1]]) &&
  !isOfType(action, 'CLEAR_STATE')
);

Epic and Middleware Integration

import { Epic } from 'redux-observable';
import { isActionOf } from 'typesafe-actions';

// Type-safe epic with action filtering
const saveUserEpic: Epic<AppAction> = (action$) =>
  action$.pipe(
    filter(isActionOf([updateUser, createUser])),
    // Actions are properly typed here
    debounceTime(500),
    switchMap(action => 
      api.saveUser(action.payload).pipe(
        map(() => saveUserSuccess()),
        catchError(error => of(saveUserFailure(error)))
      )
    )
  );

Types

/**
 * ActionCreator type used by isActionOf (specialized version)
 */
type ActionCreator<T extends { type: string }> = ((
  ...args: any[]
) => T) & ActionCreatorTypeMetadata<T['type']>;

/**
 * Type predicate function for checking action types
 */
type TypePredicate<T extends string> = <A extends { type: string }>(
  action: A
) => action is A extends { type: T } ? A : never;

/**
 * Type predicate function for checking action creators
 */
type ActionPredicate<AC extends ActionCreator<{ type: string }>> = (
  action: { type: string }
) => action is ReturnType<AC>;

/**
 * Utility type for extracting action type from action creator
 */
type ActionType<TActionCreatorOrMap extends any> = 
  TActionCreatorOrMap extends ActionCreator<infer TAction>
    ? TAction
    : TActionCreatorOrMap extends Record<any, any>
    ? { [K in keyof TActionCreatorOrMap]: ActionType<TActionCreatorOrMap[K]> }[keyof TActionCreatorOrMap]
    : never;

Install with Tessl CLI

npx tessl i tessl/npm-typesafe-actions

docs

action-creators.md

action-helpers.md

index.md

type-helpers.md

tile.json