Skip to content

Commit

Permalink
feat: action type helpers (#591)
Browse files Browse the repository at this point in the history
  • Loading branch information
sseppola authored Mar 1, 2023
1 parent 009cbd6 commit 09c6ebe
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 2 deletions.
52 changes: 52 additions & 0 deletions src/types/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import test from 'ava';
import { actionCreator } from '../actionCreator';
import { isValidRxBeachAction, isActionOfType } from './helpers';

const actionCreatorWithPayload = actionCreator<string>('[test] with payload');
const actionCreatorWithoutPayload = actionCreator('[test] no payload');

test('isValidRxBeachAction - invalid actions', (t) => {
const invalidActions = [
undefined,
null,
123,
'[test] action type',
() => {
/* noop */
},
actionCreatorWithPayload,
actionCreatorWithoutPayload,
{},
{ type: '[test] action type' },
{ type: '[test] action type', meta: {} },
{ type: '[test] action type', payload: {} },
{ meta: {}, payload: {} },
];

for (let action of invalidActions) {
t.is(isValidRxBeachAction(action), false);
}
});

test('isValidRxBeachAction - valid actions', (t) => {
const validActions = [
{ type: '[test] action type', meta: {}, payload: undefined },
actionCreatorWithPayload('asd'),
actionCreatorWithoutPayload(),
];

for (let action of validActions) {
t.is(isValidRxBeachAction(action), true);
}
});

test('isActionOfType', (t) => {
t.is(
isActionOfType(actionCreatorWithPayload, actionCreatorWithPayload('asd')),
true
);
t.is(
isActionOfType(actionCreatorWithoutPayload, actionCreatorWithoutPayload()),
true
);
});
64 changes: 63 additions & 1 deletion src/types/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { Observable } from 'rxjs';
import { ActionCreatorWithPayload } from './ActionCreator';
import {
ActionCreator,
ActionCreatorWithoutPayload,
ActionCreatorWithPayload,
} from './ActionCreator';
import { UnknownAction } from '../internal/types';
import { Action, VoidPayload } from './Action';
import { ObservableState } from '../observableState';

export type ActionStream = Observable<UnknownAction>;

Expand All @@ -18,3 +24,59 @@ export type ActionDispatcher = (
*/
export type ExtractPayload<ActionType extends ActionCreatorWithPayload<any>> =
ReturnType<ActionType>['payload'];

/**
* Assert that a value is a valid rx beach action.
* We assert that payload is a key in the object because this is what our
* actionCreator performs
*/
export const isValidRxBeachAction = (
action: unknown
): action is Action<unknown> => {
if (!action || typeof action !== 'object') {
return false;
}

return (
'type' in action &&
'meta' in action &&
'payload' in action &&
typeof action.type === 'string'
);
};

/**
* Assert that an action creator is of a specific type, and extract its payload
* type
*/
export function isActionOfType<T = VoidPayload>(
creatorFn: ActionCreator<T>,
action: unknown
): action is Action<T> {
if (!isValidRxBeachAction(action)) {
return false;
}

return action.type === creatorFn.type;
}

/**
* Type helper to infer the payload of an action creator.
* This should be preferred over exporting the types separately.
*/
export type InferPayloadFromActionCreator<TActionCreator> =
TActionCreator extends ActionCreatorWithPayload<infer TValueType>
? TValueType
: TActionCreator extends ActionCreatorWithoutPayload
? void
: never;

/**
* Type helper to get the type of the value contained in the observable
*/
export type InferValueFromObservable<TObservable> =
TObservable extends Observable<infer TValueType>
? TValueType
: TObservable extends ObservableState<infer TValueType>
? TValueType
: never;
38 changes: 37 additions & 1 deletion src/types/helpers.tspec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
import { AssertTrue, IsExact } from 'conditional-type-checks';
import { actionCreator } from '../actionCreator';
import { ActionCreatorWithPayload } from './ActionCreator';
import { ExtractPayload } from './helpers';
import {
ExtractPayload,
isActionOfType,
isValidRxBeachAction,
} from './helpers';

type Payload = { foo: number };
type ExtractPayload_extracts_payload = AssertTrue<
IsExact<ExtractPayload<ActionCreatorWithPayload<Payload>>, Payload>
>;

const actionCreatorWithPayload = actionCreator<string>('[test] with payload');
const actionCreatorWithoutPayload = actionCreator('[test] no payload');

isValidRxBeachAction(actionCreatorWithoutPayload());
isValidRxBeachAction(actionCreatorWithPayload('test'));
isValidRxBeachAction(undefined);
isValidRxBeachAction(null);
isValidRxBeachAction({});

isActionOfType(actionCreatorWithoutPayload, actionCreatorWithoutPayload());
isActionOfType(actionCreatorWithPayload, actionCreatorWithPayload('test'));
isActionOfType(actionCreatorWithPayload, undefined);
isActionOfType(actionCreatorWithPayload, null);
isActionOfType(actionCreatorWithPayload, {});

const actionPairs = [
[actionCreatorWithPayload, actionCreatorWithPayload('asd')] as const,
[actionCreatorWithoutPayload, actionCreatorWithoutPayload()] as const,
];

for (const [creatorFn, action] of actionPairs) {
// @ts-expect-error Don't know how to fix this case...
isActionOfType(creatorFn, action);
}

const action = actionCreatorWithoutPayload();
if (isActionOfType(actionCreatorWithoutPayload, action)) {
// @ts-expect-error
action.payload;
}

0 comments on commit 09c6ebe

Please sign in to comment.