Using Redux Toolkit, I have many action creators that have prepare callbacks adding a unixtime
property to the payload. This property gets the Date.now()
value. Since I'm continually adding more actions requiring a time, I'm thinking I'll instead add middleware like the following:
import { Middleware } from 'redux';
import { RootState } from '../sliceReducer'; // type of my app's Redux state
const unixtimeMiddleware: Middleware<{}, RootState> = () => (next) => (action) => {
if (action && !action.meta) {
action.meta = {};
}
action.meta.unixtime = Date.now();
return next(action);
};
export default unixtimeMiddleware;
My question is:
meta: { unixtime: number }
added to all RTK action types, or do I need to create types that extend the built-in action types like:import { PayloadAction } from '@reduxjs/toolkit';
/**
* An extension of Redux Toolkit's PayloadAction which includes a .meta.unixtime property.
*
* @template P The type of the action's payload.
* @template M Union to the action's base meta type { unixtime: number } (optional)
* @template E The type of the action's error (optional)
*
* @public
*/
export type PayloadActionUnixtime<P, M = never, E = never> = PayloadAction<
P,
string,
{ unixtime: number } & ([M] extends [never] ? {} : M),
E
>;
(Side note: I chose the name unixtime
over something like timestamp
to reduce the likelihood someone would think it is a string like YYYY-MM-DD HH-MM-SS
)
This is what I've done for over a year:
import { AnyAction, Middleware } from 'redux';
import type { RootState } from '../index';
type MetaProp = {
[moreProps: string]: unknown;
};
type TimedMetaProp = MetaProp & { unixtime: number };
// eslint-disable-next-line @typescript-eslint/ban-types
export const actionAddMetaUnixtime = <T extends {}>(
action: T
): T & { meta: TimedMetaProp } => {
const unixtime = Date.now();
const meta: TimedMetaProp = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(action && 'meta' in action ? (action as any).meta : {}),
unixtime,
};
return {
...action,
meta,
};
};
// eslint-disable-next-line @typescript-eslint/ban-types
const unixtimeMiddleware: Middleware<{}, RootState> =
() => (next) => (action: AnyAction) => {
const newAction =
typeof action === 'function'
? action // do nothing for thunk
: actionAddMetaUnixtime(action);
return next(newAction);
};
export default unixtimeMiddleware;
Then in reducers where I want to ensure that unixtime
is set, I do:
export const doFoo = (
state: RootState,
action: PayloadAction<FooProps>
): void => {
assertActionHasUnixtime(action);
// ... do foo stuff ...
};
Where assertActionHasUnixtime
is defined as:
import type { PayloadAction } from '@reduxjs/toolkit';
/**
* Assertion function that ensures that an action has `.meta.unixtime` present. All actions should
* have this, since it is added by Redux middleware. This function ensures type-safety for reducers.
*
* Alternatively we could use an action type other than PayloadAction which includes meta.unixtime,
* and that was done prior to enabling TypeScript `strictFunctionTypes`. However, with TypeScript
* `strictFunctionTypes` this failed type checking. See the following Discord discussion with the
* Redux/Redux-Toolkit maintainers where this assert method was suggested:
*
* https://discord.com/channels/102860784329052160/103538784460615680/1117937690164342824
*/
export default function assertActionHasUnixtime(
action: Record<string, unknown>
): asserts action is { meta: { unixtime: number } } {
if (
typeof action.meta === 'object' &&
!!action.meta &&
typeof (action.meta as Record<string, unknown>).unixtime === 'number'
) {
return;
}
throw new Error(
'missing .meta.unixtime in action: ' + JSON.stringify(action)
);
}