From 674c0870729af24db550a0bd5af92e9d5697959b Mon Sep 17 00:00:00 2001 From: Piotr Witek Date: Sun, 21 Apr 2019 04:28:48 +0200 Subject: [PATCH] Refactored type helpers and exported as public API Resolved #123 --- src/__snapshots__/create-reducer.spec.ts.snap | 34 +++- src/action.ts | 20 +- src/create-action-deprecated.ts | 10 +- src/create-action.ts | 10 +- src/create-async-action.ts | 20 +- src/create-custom-action.ts | 4 +- src/create-reducer.spec.ts | 8 +- src/create-reducer.ts | 70 +++---- src/create-standard-action.ts | 6 +- src/get-type.ts | 4 +- src/index.ts | 17 +- src/type-helpers.ts | 186 +++++++++--------- src/utils/validation.ts | 8 +- 13 files changed, 216 insertions(+), 181 deletions(-) diff --git a/src/__snapshots__/create-reducer.spec.ts.snap b/src/__snapshots__/create-reducer.spec.ts.snap index ad05db9..f547cea 100644 --- a/src/__snapshots__/create-reducer.spec.ts.snap +++ b/src/__snapshots__/create-reducer.spec.ts.snap @@ -1,10 +1,22 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`With Action Creators fn(0, add(4)) (type) should match snapshot 1`] = `"number"`; - -exports[`With Action Creators fn(0, add(4)) (type) should match snapshot 2`] = `"number"`; - -exports[`With Action Creators fn(0, add(4)) (type) should match snapshot 3`] = `"number"`; +exports[`With Action Creators fn(0, add(4)) (type) should match snapshot 1`] = ` +"Argument of type 'PayloadAction<\\"ADD\\", number>' is not assignable to parameter of type 'EmptyAction<\\"INCREMENT\\">'. + Types of property 'type' are incompatible. + Type '\\"ADD\\"' is not assignable to type '\\"INCREMENT\\"'." +`; + +exports[`With Action Creators fn(0, add(4)) (type) should match snapshot 2`] = ` +"Argument of type 'PayloadAction<\\"ADD\\", number>' is not assignable to parameter of type 'EmptyAction<\\"INCREMENT\\">'. + Types of property 'type' are incompatible. + Type '\\"ADD\\"' is not assignable to type '\\"INCREMENT\\"'." +`; + +exports[`With Action Creators fn(0, add(4)) (type) should match snapshot 3`] = ` +"Argument of type 'PayloadAction<\\"ADD\\", number>' is not assignable to parameter of type 'EmptyAction<\\"INCREMENT\\">'. + Types of property 'type' are incompatible. + Type '\\"ADD\\"' is not assignable to type '\\"INCREMENT\\"'." +`; exports[`With Action Creators fn(0, increment()) (type) should match snapshot 1`] = `"number"`; @@ -12,11 +24,17 @@ exports[`With Action Creators fn(0, increment()) (type) should match snapshot 2` exports[`With Action Creators fn(0, increment()) (type) should match snapshot 3`] = `"number"`; -exports[`With Action Types fn(0, add(4)) (type) should match snapshot 1`] = `"number"`; +exports[`With Action Types fn(0, {} as any) (type) should match snapshot 1`] = `"number"`; + +exports[`With Action Types fn(0, {} as any) (type) should match snapshot 2`] = `"number"`; + +exports[`With Action Types fn(0, {} as any) (type) should match snapshot 3`] = `"number"`; + +exports[`With Action Types fn(0, add(4)) (type) should match snapshot 1`] = `"Argument of type 'PayloadAction<\\"ADD\\", number>' is not assignable to parameter of type 'EmptyAction<\\"INCREMENT\\">'."`; -exports[`With Action Types fn(0, add(4)) (type) should match snapshot 2`] = `"number"`; +exports[`With Action Types fn(0, add(4)) (type) should match snapshot 2`] = `"Argument of type 'PayloadAction<\\"ADD\\", number>' is not assignable to parameter of type 'EmptyAction<\\"INCREMENT\\">'."`; -exports[`With Action Types fn(0, add(4)) (type) should match snapshot 3`] = `"number"`; +exports[`With Action Types fn(0, add(4)) (type) should match snapshot 3`] = `"Argument of type 'PayloadAction<\\"ADD\\", number>' is not assignable to parameter of type 'EmptyAction<\\"INCREMENT\\">'."`; exports[`With Action Types fn(0, increment()) (type) should match snapshot 1`] = `"number"`; diff --git a/src/action.ts b/src/action.ts index 5918d87..09cf1c1 100644 --- a/src/action.ts +++ b/src/action.ts @@ -1,4 +1,4 @@ -import { StringType } from './type-helpers'; +import { TypeConstant } from './type-helpers'; import { checkIsEmpty, throwIsEmpty, @@ -6,52 +6,52 @@ import { throwInvalidActionCreator, } from './utils/validation'; -export function action( +export function action( type: T, payload: undefined, meta: undefined, error: E ): { type: T; error: E }; -export function action( +export function action( type: T, payload: undefined, meta: M, error: E ): { type: T; meta: M; error: E }; -export function action( +export function action( type: T, payload: P, meta: undefined, error: E ): { type: T; payload: P; error: E }; -export function action( +export function action( type: T, payload: P, meta: M, error: E ): { type: T; payload: P; meta: M; error: E }; -export function action( +export function action( type: T, payload: undefined, meta: M ): { type: T; meta: M }; -export function action( +export function action( type: T, payload: P, meta: M ): { type: T; payload: P; meta: M }; -export function action( +export function action( type: T, payload: P ): { type: T; payload: P }; -export function action(type: T): { type: T }; +export function action(type: T): { type: T }; /** * @description flux standard action factory @@ -61,7 +61,7 @@ export function action(type: T): { type: T }; * ``` */ export function action< - T extends StringType, + T extends TypeConstant, P = undefined, M = undefined, E = undefined diff --git a/src/create-action-deprecated.ts b/src/create-action-deprecated.ts index 8338640..0e8afa7 100644 --- a/src/create-action-deprecated.ts +++ b/src/create-action-deprecated.ts @@ -2,9 +2,9 @@ * DEPRECATED */ -import { StringType } from './type-helpers'; +import { TypeConstant } from './type-helpers'; -interface FSA { +interface FSA { type: T; payload?: P; meta?: M; @@ -16,7 +16,7 @@ interface FSA { * @description create an action-creator of a given function that contains hidden "type" metadata */ export function createActionDeprecated< - T extends StringType, + T extends TypeConstant, AC extends (...args: any[]) => FSA >(actionType: T, creatorFunction: AC): AC; @@ -25,7 +25,7 @@ export function createActionDeprecated< * @description create an action-creator of a given function that contains hidden "type" metadata */ export function createActionDeprecated< - T extends StringType, + T extends TypeConstant, AC extends () => { type: T } >(actionType: T): AC; @@ -33,7 +33,7 @@ export function createActionDeprecated< * implementation */ export function createActionDeprecated< - T extends StringType, + T extends TypeConstant, AC extends (...args: any[]) => FSA >(actionType: T, creatorFunction?: AC): AC { let actionCreator: AC; diff --git a/src/create-action.ts b/src/create-action.ts index d948fc8..cd08e6b 100644 --- a/src/create-action.ts +++ b/src/create-action.ts @@ -1,7 +1,11 @@ -import { StringType, ActionCreator } from './type-helpers'; +import { TypeConstant, ActionCreator } from './type-helpers'; import { action } from './action'; -export type PayloadMetaAction = P extends undefined +export type PayloadMetaAction< + T extends TypeConstant, + P, + M +> = P extends undefined ? M extends undefined ? { type: T } : { type: T; meta: M } @@ -13,7 +17,7 @@ export type PayloadMetaAction = P extends undefined * @description typesafe action-creator factory */ export function createAction< - T extends StringType, + T extends TypeConstant, AC extends ActionCreator = () => { type: T } >( type: T, diff --git a/src/create-async-action.ts b/src/create-async-action.ts index 65b26d7..ad761b3 100644 --- a/src/create-async-action.ts +++ b/src/create-async-action.ts @@ -1,5 +1,5 @@ import { - StringType, + TypeConstant, ActionBuilderConstructor, // ActionBuilderMap, } from './type-helpers'; @@ -7,9 +7,9 @@ import { createCustomAction } from './create-custom-action'; import { checkInvalidActionTypeInArray } from './utils/validation'; export interface AsyncActionBuilder< - T1 extends StringType, - T2 extends StringType, - T3 extends StringType + T1 extends TypeConstant, + T2 extends TypeConstant, + T3 extends TypeConstant > { // tslint:disable-next-line:callable-types (): AsyncActionBuilderConstructor; @@ -21,9 +21,9 @@ export interface AsyncActionBuilder< } export type AsyncActionBuilderConstructor< - T1 extends StringType, - T2 extends StringType, - T3 extends StringType, + T1 extends TypeConstant, + T2 extends TypeConstant, + T3 extends TypeConstant, P1, P2, P3 @@ -37,9 +37,9 @@ export type AsyncActionBuilderConstructor< * implementation */ export function createAsyncAction< - T1 extends StringType, - T2 extends StringType, - T3 extends StringType + T1 extends TypeConstant, + T2 extends TypeConstant, + T3 extends TypeConstant >( requestType: T1, successType: T2, diff --git a/src/create-custom-action.ts b/src/create-custom-action.ts index 7894501..027d89e 100644 --- a/src/create-custom-action.ts +++ b/src/create-custom-action.ts @@ -1,4 +1,4 @@ -import { ActionCreator, StringType } from './type-helpers'; +import { ActionCreator, TypeConstant } from './type-helpers'; import { checkIsEmpty, throwIsEmpty, @@ -10,7 +10,7 @@ import { * @description create custom action-creator using constructor function with injected type argument */ export function createCustomAction< - T extends StringType, + T extends TypeConstant, AC extends ActionCreator = () => { type: T } >(type: T, createHandler?: (type: T) => AC): AC { if (checkIsEmpty(type)) { diff --git a/src/create-reducer.spec.ts b/src/create-reducer.spec.ts index 66db905..9f9ec36 100644 --- a/src/create-reducer.spec.ts +++ b/src/create-reducer.spec.ts @@ -1,10 +1,12 @@ -import { ActionType, createStandardAction, createReducer } from '.'; +import { createStandardAction } from './create-standard-action'; +import { createReducer } from './create-reducer'; +import { ActionType } from './type-helpers'; const add = createStandardAction('ADD')(); const increment = createStandardAction('INCREMENT')(); const actions = { add, increment }; -declare module '.' { +declare module './' { export type RootAction = ActionType; } @@ -64,6 +66,8 @@ describe('With Action Types', () => { }); [counterReducer1, counterReducer2, counterReducer3].forEach(fn => { + // @dts-jest:pass:snap + fn(0, {} as any); // => 0 // @dts-jest:pass:snap fn(0, increment()); // => 1 // @dts-jest:pass:snap diff --git a/src/create-reducer.ts b/src/create-reducer.ts index 7a44d5c..49fe5e5 100644 --- a/src/create-reducer.ts +++ b/src/create-reducer.ts @@ -1,44 +1,41 @@ // @ts-ignore -import { RootAction } from '.'; +import { RootAction } from './'; import { getType } from './get-type'; import { checkValidActionCreator, checkValidActionType, throwInvalidActionTypeOrActionCreator, } from './utils/validation'; +import { Reducer, Action } from './type-helpers'; -export function createReducer( +type AddHandler = < + TType extends TAllActions['type'], + TTypeAction extends TAllActions extends { type: TType } ? TAllActions : never, + TCreator extends (...args: any[]) => TAllActions, + TCreatorAction extends TAllActions extends ReturnType + ? TAllActions + : never, + TActionIntersection extends TTypeAction extends TCreatorAction + ? TTypeAction + : never +>( + actionsTypes: TType | TCreator | TType[] | TCreator[], + actionsHandler: (state: S, action: TActionIntersection) => S +) => Exclude extends never + ? Reducer + : Reducer & { + addHandler: AddHandler< + S, + Exclude + >; + }; + +export function createReducer( initialState: S ) { - type AddHandler = < - TType extends TAllActions['type'], - TTypeAction extends TAllActions extends { type: TType } - ? TAllActions - : never, - TCreator extends (...args: any[]) => TAllActions, - TCreatorAction extends TAllActions extends ReturnType - ? TAllActions - : never, - TActionIntersection extends TTypeAction extends TCreatorAction - ? TTypeAction - : never - >( - actionsTypes: TType | TCreator | TType[] | TCreator[], - actionsHandler: (state: S, action: TActionIntersection) => S - ) => Exclude extends never - ? Reducer - : Reducer & { - addHandler: AddHandler< - Exclude - >; - }; - - type AddHandlerChain = { addHandler: AddHandler }; + const handlers: Record S> = {}; - const handlers: Record = {}; - - type Reducer = (state: S, action: A) => S; - const reducer: Reducer = (state = initialState, action) => { + const reducer: Reducer = (state = initialState, action) => { if (handlers.hasOwnProperty(action.type)) { return handlers[action.type](state, action); } else { @@ -46,7 +43,7 @@ export function createReducer( } }; - const addHandler = ((actionsTypes, actionsHandler: Reducer) => { + const addHandler = ((actionsTypes, actionsHandler) => { const creatorsOrTypes = Array.isArray(actionsTypes) ? actionsTypes : [actionsTypes]; @@ -60,12 +57,15 @@ export function createReducer( : throwInvalidActionTypeOrActionCreator() ) .forEach(type => (handlers[type] = actionsHandler)); - return chain; - }) as AddHandler; - const chain: Reducer & AddHandlerChain = Object.assign(reducer, { + return chainApi; + }) as AddHandler; + + const chainApi: Reducer & { + addHandler: AddHandler; + } = Object.assign(reducer, { addHandler, }); - return chain; + return chainApi; } diff --git a/src/create-standard-action.ts b/src/create-standard-action.ts index 61d553f..95f281d 100644 --- a/src/create-standard-action.ts +++ b/src/create-standard-action.ts @@ -1,5 +1,5 @@ import { - StringType, + TypeConstant, ActionBuilderConstructor, ActionBuilderMap, } from './type-helpers'; @@ -11,7 +11,7 @@ import { throwInvalidActionType, } from './utils/validation'; -export interface ActionBuilder { +export interface ActionBuilder {

(): ActionBuilderConstructor; map( fn: (payload: P, meta: M) => R @@ -21,7 +21,7 @@ export interface ActionBuilder { /** * @description create an action-creator of a given function that contains hidden "type" metadata */ -export function createStandardAction( +export function createStandardAction( type: T ): ActionBuilder { if (checkIsEmpty(type)) { diff --git a/src/get-type.ts b/src/get-type.ts index 2ffccd6..5b4f8ff 100644 --- a/src/get-type.ts +++ b/src/get-type.ts @@ -1,4 +1,4 @@ -import { StringType, ActionCreator, TypeMeta } from './type-helpers'; +import { TypeConstant, ActionCreator, TypeMeta } from './type-helpers'; import { checkIsEmpty, throwIsEmpty, @@ -9,7 +9,7 @@ import { /** * @description get the "type literal" of a given action-creator */ -export function getType( +export function getType( actionCreator: ActionCreator & TypeMeta ): T { if (checkIsEmpty(actionCreator)) { diff --git a/src/index.ts b/src/index.ts index e98d62b..1797069 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,22 @@ export { isOfType } from './is-of-type'; export { isActionOf } from './is-action-of'; // type-helpers -export { ActionType, StateType, ActionCreator, TypeMeta } from './type-helpers'; +export { + TypeConstant, + Action, + ActionCreator, + Reducer, + EmptyAction, + PayloadAction, + MetaAction, + PayloadMetaAction, + EmptyAC, + PayloadAC, + PayloadMetaAC, + TypeMeta, + ActionType, + StateType, +} from './type-helpers'; // deprecated export { createActionDeprecated } from './create-action-deprecated'; diff --git a/src/type-helpers.ts b/src/type-helpers.ts index ac39414..a4aa2bc 100644 --- a/src/type-helpers.ts +++ b/src/type-helpers.ts @@ -2,151 +2,145 @@ * PUBLIC API */ -export interface TypeMeta { - getType?: () => T; -} - -export type ActionCreator = ( - ...args: any[] -) => { type: T }; - /** - * @desc Infers Action union-type from action-creator map object + * @desc Type representing Type Constant */ -export type ActionType< - ActionCreatorOrMap extends any -> = ActionCreatorOrMap extends ActionCreator - ? ReturnType - : ActionCreatorOrMap extends Record - ? { - [K in keyof ActionCreatorOrMap]: ActionType - }[keyof ActionCreatorOrMap] - : ActionCreatorOrMap extends infer R // should be just never but compiler yell with circularly references itself error - ? never - : never; +export type TypeConstant = string; /** - * @desc Infers State object from reducer map object + * @desc Type representing Generic Action */ -export type StateType = ReducerOrMap extends ( - ...args: any[] -) => any - ? ReturnType - : ReducerOrMap extends object - ? { [K in keyof ReducerOrMap]: StateType } - : never; +export type Action = { + type: TType; +}; /** - * INTERNAL API + * @desc Type representing Generic ActionCreator */ +export type ActionCreator = ( + ...args: any[] +) => Action; /** - * @private - * @desc Representing action-type of string + * @desc Type representing Generic Reducer */ -export type StringType = string; +export type Reducer = ( + state: TState | undefined, + action: TAction +) => TState; /** - * @private * @desc Action without Payload - * @type T - ActionType */ -export type EmptyAction = { - type: T; +export type EmptyAction = { + type: TType; }; /** - * @private * @desc Action with only Payload - * @type T - ActionType - * @type P - Payload */ -export type PayloadAction = { - type: T; - payload: P; +export type PayloadAction = { + type: TType; + payload: TPayload; }; /** - * @private * @desc Action with only Meta - * @type T - ActionType - * @type P - Payload - * @type M - Meta */ -export type MetaAction = { - type: T; - meta: M; +export type MetaAction = { + type: TType; + meta: TMeta; }; /** - * @private * @desc Action with both Payload and Meta - * @type T - ActionType - * @type P - Payload - * @type M - Meta - */ -export type PayloadMetaAction = { - type: T; - payload: P; - meta: M; + */ +export type PayloadMetaAction = { + type: TType; + payload: TPayload; + meta: TMeta; }; /** - * TODO: NOT USED - * @private - * @desc Flux Standard Action - * @type T - ActionType - * @type P - Payload - * @type M - Meta - */ -export interface FluxStandardAction< - T extends StringType, - P = undefined, - M = undefined -> { - type: T; - payload: P; - meta: M; - error?: true; + * @desc Action Creator producing EmptyAction + */ +export type EmptyAC = () => EmptyAction; + +/** + * @desc Action Creator producing PayloadAction + */ +export type PayloadAC = ( + payload: TPayload +) => PayloadAction; + +/** + * @desc Action Creator producing PayloadMetaAction + */ +export type PayloadMetaAC = ( + payload: TPayload, + meta: TMeta +) => PayloadMetaAction; + +/** + * @desc Type representing type getter on Action Creator instance + */ +export interface TypeMeta { + getType?: () => TType; } -/** @private */ -export type EmptyAC = () => EmptyAction; +/** + * @desc Infers Action union-type from action-creator map object + */ +export type ActionType< + TActionCreatorOrMap extends any +> = TActionCreatorOrMap extends ActionCreator + ? ReturnType + : TActionCreatorOrMap extends Record + ? { + [K in keyof TActionCreatorOrMap]: ActionType + }[keyof TActionCreatorOrMap] + : TActionCreatorOrMap extends infer R // TODO: should be just never but compiler yell with circularly references itself error + ? never + : never; -/** @private */ -export type PayloadAC = ( - payload: P -) => PayloadAction; +/** + * @desc Infers State object from reducer map object + */ +export type StateType< + TReducerOrMap extends any +> = TReducerOrMap extends Reducer + ? ReturnType + : TReducerOrMap extends Record + ? { [K in keyof TReducerOrMap]: StateType } + : never; -/** @private */ -export type PayloadMetaAC = ( - payload: P, - meta: M -) => PayloadMetaAction; +/** + * INTERNAL API + */ /** @private */ export type ActionBuilderConstructor< - T extends StringType, + TType extends TypeConstant, TPayload extends any = undefined, TMeta extends any = undefined > = [TMeta] extends [undefined] ? [TPayload] extends [undefined] ? unknown extends TPayload - ? PayloadAC + ? PayloadAC : unknown extends TMeta - ? PayloadMetaAC - : EmptyAC - : PayloadAC - : PayloadMetaAC; + ? PayloadMetaAC + : EmptyAC + : PayloadAC + : PayloadMetaAC; /** @private */ export type ActionBuilderMap< - T extends StringType, - TCustomAction extends any, + TType extends TypeConstant, + TActionProps extends any, TPayloadArg extends any = undefined, TMetaArg extends any = undefined > = [TMetaArg] extends [undefined] ? [TPayloadArg] extends [undefined] - ? () => { type: T } & TCustomAction - : (payload: TPayloadArg) => { type: T } & TCustomAction - : (payload: TPayloadArg, meta: TMetaArg) => { type: T } & TCustomAction; + ? () => { type: TType } & TActionProps + : (payload: TPayloadArg) => { type: TType } & TActionProps + : (payload: TPayloadArg, meta: TMetaArg) => { type: TType } & TActionProps; diff --git a/src/utils/validation.ts b/src/utils/validation.ts index cd4b3a6..de99ed7 100644 --- a/src/utils/validation.ts +++ b/src/utils/validation.ts @@ -1,4 +1,4 @@ -import { ActionCreator, TypeMeta, StringType } from '../type-helpers'; +import { ActionCreator, TypeMeta, TypeConstant } from '../type-helpers'; export function checkIsEmpty(arg: unknown, argPosition: number = 1) { return arg == null; @@ -10,7 +10,7 @@ export function throwIsEmpty(argPosition: number = 1): never { export function checkValidActionCreator( arg: unknown -): arg is ActionCreator { +): arg is ActionCreator { return typeof arg === 'function' && 'getType' in arg; } @@ -25,7 +25,7 @@ export function throwInvalidActionCreator(argPosition: number = 1): never { } export function checkInvalidActionCreatorInArray( - arg: ActionCreator & TypeMeta, + arg: ActionCreator & TypeMeta, idx: number ): void | never { if (arg == null) { @@ -55,7 +55,7 @@ export function throwInvalidActionType(argPosition: number = 1): never { } export function checkInvalidActionTypeInArray( - arg: StringType, + arg: TypeConstant, idx: number ): void | never { if (arg == null) {