Skip to content

Commit

Permalink
Merge pull request #13 from dabapps/type-overhaul
Browse files Browse the repository at this point in the history
Type overhaul
  • Loading branch information
JakeSidSmith authored Aug 21, 2020
2 parents c02607e + 4e150fa commit 301df9e
Show file tree
Hide file tree
Showing 5 changed files with 39 additions and 61 deletions.
40 changes: 6 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,50 +18,22 @@ npm i @dabapps/redux-create-reducer -S

## Usage

```js
```ts
import { createReducer } from '@dabapps/redux-create-reducer';
import { ADD, SUBTRACT, RESET } from './action-types';

export const count = createReducer(
{
[ADD]: (state, action) => state + action.payload,
[SUBTRACT]: (state, action) => state - action.payload,
[RESET]: () => 0,
},
0 // Initial state (required)
);
```

## TypeScript

You can supply a generic type for the reducer state, and the actions it should handle:

```ts
interface NumberAction {
type: string;
payload: number;
}

export const count = createReducer<number, NumberAction>(
// ...
);
```

You can use the action generic parameter to narrow the available handlers by setting specific `type` keys:

```ts
interface NumberAction {
type: 'ADD' | 'SUBTRACT' | 'RESET';
payload: number;
}

export const count = createReducer<number, NumberAction>(
export const count = createReducer(
{
// ...
// The next line will have a type error because MULTIPLY was not defined in our type interface
[MULTIPLY]: (state, action) => state * action.payload,
[ADD]: (state: number, action: NumberAction) => state + action.payload,
[SUBTRACT]: (state: number, action: NumberAction) => state - action.payload,
[RESET]: () => 0,
},
// ...
0 // Initial state (required)
);
```

Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dabapps/redux-create-reducer",
"version": "1.0.4",
"version": "1.1.0",
"description": "A utility to create redux reducers from a set of handlers",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
26 changes: 16 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ const INVALID_HANDLER_KEYS = ['undefined', 'null'];

type NotUndefined = {} | null;

export type Handlers<S extends NotUndefined, A extends Action = AnyAction> = {
[P in A['type']]: (state: S, action: A) => S;
};
type ActionOfType<A extends Action, T> = Exclude<A, Exclude<A, { type: T }>>;

function validateKeys(handlers: Handlers<any, any>) {
function validateKeys(handlers: Record<string, unknown>) {
INVALID_HANDLER_KEYS.forEach(key => {
if (Object.prototype.hasOwnProperty.call(handlers, key)) {
throw new Error(`Invalid createReducer handler key: ${key}`);
Expand All @@ -18,11 +16,18 @@ function validateKeys(handlers: Handlers<any, any>) {

export function createReducer<
S extends NotUndefined,
A extends Action = AnyAction
>(handlers: Handlers<S, A>, initialState: S): Reducer<S, A> {
A extends Action<T>,
T extends string | symbol
>(
handlers: {
[P in T]: (state: S, action: ActionOfType<A, P>) => S;
},
initialState: S
) {
if (
!handlers ||
typeof (handlers as Handlers<S, A> | undefined) !== 'object' ||
// tslint:disable-next-line:strict-type-predicates
typeof handlers !== 'object' ||
Array.isArray(handlers)
) {
throw new Error(
Expand All @@ -36,13 +41,14 @@ export function createReducer<

validateKeys(handlers);

return (state: S = initialState, action: A): S => {
return ((state: S = initialState, action: ActionOfType<A, T>): S => {
const { type } = action;

if (Object.prototype.hasOwnProperty.call(handlers, type)) {
return handlers[type as keyof Handlers<S, A>](state, action);
const handler = handlers[type];
return handler(state, action);
}

return state;
};
}) as Reducer<S, AnyAction>;
}
30 changes: 15 additions & 15 deletions tests/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createReducer, Handlers } from '../src';
import { createReducer } from '../src';

describe('createReducer', () => {
const MATCHES_INVALID_KEY = /\binvalid\b.+\bkey\b/i;
Expand All @@ -12,9 +12,9 @@ describe('createReducer', () => {
});

it('returns the initialState when an unknown action is emitted', () => {
const reducer = createReducer<number>(
const reducer = createReducer(
{
foo: state => state + 1,
foo: (state: number) => state + 1,
},
0
);
Expand Down Expand Up @@ -42,15 +42,15 @@ describe('createReducer', () => {
});

it('should error if the handlers are not a string keyed object', () => {
expect(() =>
createReducer((null as unknown) as Handlers<any, any>, null)
).toThrow(MATCHES_INVALID_HANDLERS);
expect(() =>
createReducer(([] as unknown) as Handlers<any, any>, null)
).toThrow(MATCHES_INVALID_HANDLERS);
expect(() =>
createReducer((undefined as unknown) as Handlers<any, any>, null)
).toThrow(MATCHES_INVALID_HANDLERS);
expect(() => createReducer(null as any, null)).toThrow(
MATCHES_INVALID_HANDLERS
);
expect(() => createReducer([] as any, null)).toThrow(
MATCHES_INVALID_HANDLERS
);
expect(() => createReducer(undefined as any, null)).toThrow(
MATCHES_INVALID_HANDLERS
);
});

it('should handler multiple actions (including symbols)', () => {
Expand All @@ -70,10 +70,10 @@ describe('createReducer', () => {
payload: count,
});

const reducer = createReducer<number, NumberAction>(
const reducer = createReducer(
{
[ADD]: (state, action) => state + action.payload,
[SUB]: (state, action) => state - action.payload,
[ADD]: (state: number, action: NumberAction) => state + action.payload,
[SUB]: (state: number, action: NumberAction) => state - action.payload,
},
5
);
Expand Down

0 comments on commit 301df9e

Please sign in to comment.