Typesafe Action Creators for Redux / Flux Architectures in TypeScript
—
Utilities for type-safe action inspection and filtering, including type guards for discriminated union types and action creator type extraction for Redux/Flux architectures.
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;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))
);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)))
)
)
);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')
);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)))
)
)
);/**
* 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