Skip to content

Commit

Permalink
fix: helper exports (#597)
Browse files Browse the repository at this point in the history
  • Loading branch information
sseppola authored Mar 9, 2023
1 parent 22cd985 commit 40f67a0
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 132 deletions.
55 changes: 54 additions & 1 deletion src/actionCreator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import test from 'ava';
import { actionCreator } from './actionCreator';
import {
actionCreator,
isActionOfType,
isValidRxBeachAction,
} from './actionCreator';

type Payload = { num: number };
const myAction = actionCreator<Payload>('[test] three');
Expand Down Expand Up @@ -54,3 +58,52 @@ test('actionCreator should create action objects with protected namespace', (t)
{ instanceOf: TypeError }
);
});

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 (const invalidAction of invalidActions) {
t.is(isValidRxBeachAction(invalidAction), false);
}
});

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

for (const validAction of validActions) {
t.is(isValidRxBeachAction(validAction), true);
}
});

test('isActionOfType', (t) => {
t.is(
isActionOfType(actionCreatorWithPayload, actionCreatorWithPayload('asd')),
true
);
t.is(
isActionOfType(actionCreatorWithoutPayload, actionCreatorWithoutPayload()),
true
);
});
37 changes: 37 additions & 0 deletions src/actionCreator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Action, VoidPayload } from './types/Action';
import {
ActionCreator,
ActionCreatorWithPayload,
ActionCreatorWithoutPayload,
} from './types/ActionCreator';
Expand Down Expand Up @@ -44,3 +46,38 @@ export const actionCreator: ActionCreatorFunc = (type: string) => {

return Object.freeze(actionCreatorFn);
};

/**
* 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 const isActionOfType = <T = VoidPayload>(
creatorFn: ActionCreator<T>,
action: unknown
): action is Action<T> => {
if (!isValidRxBeachAction(action)) {
return false;
}

return action.type === creatorFn.type;
};
56 changes: 55 additions & 1 deletion src/actionCreator.tspec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { actionCreator } from './actionCreator';
import {
actionCreator,
isActionOfType,
isValidRxBeachAction,
} from './actionCreator';

const booleanAction = actionCreator<boolean>('[test] boolean');
booleanAction(true);
Expand Down Expand Up @@ -51,3 +55,53 @@ const optionalPayloadAction = actionCreator<string | void>(

optionalPayloadAction();
optionalPayloadAction('payload');

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

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const typeChecks = [
// accepts results from action creators
isValidRxBeachAction(actionCreatorWithoutPayload()),
isValidRxBeachAction(actionCreatorWithPayload('test')),

// should accept any value as argument
isValidRxBeachAction(undefined),
isValidRxBeachAction(null),
isValidRxBeachAction({}),

// @ts-expect-error: first argument of isActionOfType should be an action creator
isActionOfType({}, actionCreatorWithoutPayload()),
// @ts-expect-error: first argument of isActionOfType should be an action creator
isActionOfType(1, actionCreatorWithoutPayload()),
// @ts-expect-error: first argument of isActionOfType should be an action creator
isActionOfType(null, actionCreatorWithoutPayload()),
// @ts-expect-error: first argument of isActionOfType should be an action creator
isActionOfType(undefined, actionCreatorWithoutPayload()),

// second argument of isActionOfType accepts anything
isActionOfType(actionCreatorWithoutPayload, actionCreatorWithoutPayload()),
isActionOfType(actionCreatorWithoutPayload, actionCreatorWithPayload('test')),
isActionOfType(actionCreatorWithPayload, 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, anAction] of actionPairs) {
// @ts-expect-error Don't know how to fix this case...
isActionOfType(creatorFn, anAction);
}

const action1 = actionCreatorWithoutPayload();
if (isActionOfType(actionCreatorWithoutPayload, action1)) {
// @ts-expect-error Assert that ActionWithoutPayload does not have a payload
// eslint-disable-next-line no-unused-expressions
action1.payload;
}
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
export { action$, dispatchAction } from './action$';

export {
export type {
ActionWithoutPayload,
ActionWithPayload,
Action,
} from './types/Action';
export {
export type {
ActionCreatorWithoutPayload,
ActionCreatorWithPayload,
ActionCreator,
} from './types/ActionCreator';
export {
export type {
ActionStream,
ActionDispatcher,
ExtractPayload,
InferValueFromObservable,
InferPayloadFromActionCreator,
} from './types/helpers';

export { actionCreator } from './actionCreator';
Expand Down
52 changes: 0 additions & 52 deletions src/types/helpers.spec.ts

This file was deleted.

37 changes: 0 additions & 37 deletions src/types/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Observable } from 'rxjs';
import {
ActionCreator,
ActionCreatorWithPayload,
ActionCreatorWithoutPayload,
} from './ActionCreator';
import { UnknownAction } from '../internal/types';
import { Action, VoidPayload } from './Action';
import { ObservableState } from '../observableState';

export type ActionStream = Observable<UnknownAction>;
Expand All @@ -25,41 +23,6 @@ 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 const 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.
Expand Down
39 changes: 1 addition & 38 deletions src/types/helpers.tspec.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,8 @@
import { AssertTrue, IsExact } from 'conditional-type-checks';
import { actionCreator } from '../actionCreator';
import { ActionCreatorWithPayload } from './ActionCreator';
import {
ExtractPayload,
isActionOfType,
isValidRxBeachAction,
} from './helpers';
import { ExtractPayload } 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 Assert that ActionWithoutPayload does not have a payload
// eslint-disable-next-line no-unused-expressions
action.payload;
}

0 comments on commit 40f67a0

Please sign in to comment.