From 81d8e8186cd14fa83439ff36e6e9f8d531d63136 Mon Sep 17 00:00:00 2001 From: gillchristian Date: Thu, 13 Jun 2019 13:42:38 +0200 Subject: [PATCH] feature(types): Add Handler type --- readme.md | 13 +++++++--- src/index.spec.ts | 65 +++++++++++++++++++++++++++++++++++------------ src/index.ts | 30 ++++++++++++---------- src/utils.ts | 11 ++++---- 4 files changed, 82 insertions(+), 37 deletions(-) diff --git a/readme.md b/readme.md index c04b376..8c815eb 100644 --- a/readme.md +++ b/readme.md @@ -55,7 +55,7 @@ Handle the actions: ```ts // src/pages/MyPage/reducer.ts -import { handleActions } from '@housinganywhere/safe-redux'; +import { handleActions, Handler } from '@housinganywhere/safe-redux'; import { User } from '../types'; @@ -69,11 +69,17 @@ const initialState: State = { count: 0, }; +// `Handler` type can be used when you don't want to define the handlers inline +const handleIncBy: Handler = ( + { count }, + { payload }, +) => ({ count: count + payload }); + const reducer = handleActions( { [INC]: ({ count }) => ({ count: count + 1 }), [DEC]: ({ count }) => ({ count: count - 1 }), - [INC_BY]: ({ count }, { payload }) => ({ count: count + payload }), + [INC_BY]: handleIncBy, [WITH_META]: ({ count }, { payload }) => ({ count: count + payload }), }, initialState, @@ -140,4 +146,5 @@ export default connect( - Added `handleActions` to create type safe reducers. - Smaller API. `safe-redux` only exports a few functions and types: - Functions: `createAction` and `handleActions`. - - Types: `Action`, `ActionsUnion`, `ActionsOfType` and `BindAction`. + - Types: `Action`, `ActionsUnion`, `ActionsOfType`, `Handler` and + `BindAction`. diff --git a/src/index.spec.ts b/src/index.spec.ts index f8cc6e3..233b874 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,4 +1,4 @@ -import { createAction, handleActions } from './index'; +import { createAction, handleActions, ActionsUnion, Handler } from './index'; describe('handleActions', () => { const ACTION = 'SOME_ACTION'; @@ -12,8 +12,8 @@ describe('handleActions', () => { initialState, ); - it('should handle actions', () => { - const action = { type: ACTION }; + it('handles actions', () => { + const action = { type: ACTION, error: false }; const actual = reducer(initialState, action); @@ -21,51 +21,51 @@ describe('handleActions', () => { expect(actionHandler).toBeCalledWith(initialState, action); }); - it('should return state when action type is not handled', () => { - const action = { type: 'other_action' }; - const state = { foo: 'foo' }; + it('returns state when action type is not handled', () => { + const action = { type: 'other_action', error: false }; + const state = { foo: 'foo', error: false }; const actual = reducer(state, action); expect(actual).toBe(state); }); - it('should default to initialState when state is undefined', () => { - const action = { type: ACTION }; + it('defaults to initialState when state is undefined', () => { + const action = { type: ACTION, error: false }; reducer(undefined, action); expect(actionHandler).toBeCalledWith(initialState, action); }); - it('should return initialState when state is undefined and action is not handled', () => { - const actual = reducer(undefined, { type: '@@REDUX/INIT' }); + it('returns initialState when state is undefined and action is not handled', () => { + const actual = reducer(undefined, { type: '@@REDUX/INIT', error: false }); expect(actual).toBe(initialState); }); }); describe('createAction', () => { - it('should create an action with only type and error props', () => { + it('creates an action with only type and error props', () => { const action = createAction('action-type'); expect(action).toEqual({ type: 'action-type', error: false }); }); - it('should create an action false error prop', () => { + it('creates an action false error prop', () => { const action = createAction('action-type'); expect(action.error).toBe(false); }); - it('should create an action with type, payload and error props', () => { + it('creates an action with type, payload and error props', () => { const payload = { foo: 'bar' }; const action = createAction('action-type', payload); expect(action).toEqual({ type: 'action-type', payload, error: false }); }); - it('should create an action true error prop', () => { + it('creates an action true error prop', () => { const payload = new Error('error'); const action = createAction('action-type', payload); @@ -73,7 +73,7 @@ describe('createAction', () => { expect(action).toEqual({ type: 'action-type', payload, error: true }); }); - it('should create an action with type, payload, meta and error props', () => { + it('creates an action with type, payload, meta and error props', () => { const payload = { foo: 'bar' }; const meta = { foo: 'bar' }; const action = createAction('action-type', payload, meta); @@ -86,7 +86,7 @@ describe('createAction', () => { }); }); - it('should create an action true error prop and meta', () => { + it('creates an action true error prop and meta', () => { const payload = new Error('error'); const meta = { foo: 'bar' }; const action = createAction('action-type', payload, meta); @@ -100,3 +100,36 @@ describe('createAction', () => { }); }); }); + +// type tests, all these should type check + +interface State { + foo: string; +} + +const enum ActionTypes { + foo = 'foo', + bar = 'bar', + baz = 'baz', +} + +const Actions = { + foo: () => createAction(ActionTypes.foo), + bar: (s: string) => createAction(ActionTypes.bar, s), + baz: (n: number) => createAction(ActionTypes.baz, n), +}; +type Actions = ActionsUnion; + +const handleBaz: Handler = ( + s, + { payload }, +) => ({ foo: s.foo + payload.toString() }); + +const reducer = handleActions( + { + foo: () => ({ foo: 'foo' }), + bar: (s, { payload }) => ({ foo: s.foo + payload }), + baz: handleBaz, + }, + { foo: 'bar' }, +); diff --git a/src/index.ts b/src/index.ts index dba7e17..bb5b36f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export { BindAction } from './utils'; -import { AnyFunction, StringMap } from './utils'; +import { ReturnType } from './utils'; // We use conditional types so we can have only one type for defining Action export type Action< @@ -14,9 +14,10 @@ export type Action< ? Readonly<{ type: T; payload: P; error: boolean }> : Readonly<{ type: T; payload: P; meta: M; error: boolean }>; -export type ActionsUnion> = ReturnType< - A[keyof A] ->; +type ActionCreator = (...args: any[]) => Action; +type ActionCreators = { [k: string]: ActionCreator }; + +export type ActionsUnion = ReturnType; // conditional type for filtering actions in epics/effects export type ActionsOfType< @@ -24,6 +25,11 @@ export type ActionsOfType< ActionType extends string > = ActionUnion extends Action ? ActionUnion : never; +export type Handler = ( + state: State, + action: ActionsOfType, +) => State; + export function createAction(type: T): Action; export function createAction( type: T, @@ -51,13 +57,11 @@ export function createAction( export function handleActions< State, Types extends string, - Actions extends ActionsUnion<{ [T in Types]: AnyFunction }> ->( - handler: { - [T in Types]: (state: State, action: ActionsOfType) => State; - }, - initialState: State, -) { - return (state = initialState, action: Actions): State => - handler[action.type] ? handler[action.type](state, action) : state; + Actions extends ActionsUnion<{ [T in Types]: ActionCreator }> +>(handlers: { [T in Types]: Handler }, initialState: State) { + return (state = initialState, action: Actions): State => { + const handler = handlers[action.type]; + + return handler ? handler(state, action) : state; + }; } diff --git a/src/utils.ts b/src/utils.ts index 1b3e9bc..aeac8d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -85,14 +85,15 @@ type ChangeReturnType = F extends () => any ) => R : never; -export type AnyFunction = (...args: any[]) => any; +// TS `ReturnType` defaults to `any` instead of `never` +export type ReturnType any> = T extends ( + ...args: any[] +) => infer R + ? R + : never; // tslint:enable no-any // use case example: // @link http://bit.ly/2KSnhEK export type BindAction = ChangeReturnType; - -export interface StringMap { - [key: string]: T; -}