From da12d9febcc8b1d95661076a268073a3161f3c68 Mon Sep 17 00:00:00 2001 From: Surgie Finesse Date: Mon, 12 Aug 2019 12:21:35 +0300 Subject: [PATCH 1/4] fix: type check issues --- .eslintignore | 1 + types/index.d.ts | 20 +++++---- types/test.ts | 106 +++++++++++++++++++++++----------------------- types/tslint.json | 2 +- 4 files changed, 66 insertions(+), 63 deletions(-) diff --git a/.eslintignore b/.eslintignore index fc06a73..dda766a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ lib coverage .nyc_output +types diff --git a/types/index.d.ts b/types/index.d.ts index e8167c0..a82adf9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,24 +1,26 @@ // TypeScript Version: 2.8 +export {}; + export interface NamespaceOptions { - namespace?: string - defaultReducer?: (prevState: State, action: Action) => State - separator?: string + namespace?: string; + defaultReducer?: (prevState: State, action: Action) => State; + separator?: string; } -export type Symbiote = (state: State, ...payload: any[]) => State +export type Symbiote = (state: State, ...payload: any[]) => State; -export type Reducer = (state: State, action: Action) => State +export type Reducer = (state: State, action: Action) => State; export type ActionsConfig = { [Key in keyof Actions]: Actions[Key] extends Function // tslint:disable-line ? Symbiote : ActionsConfig -} +}; export interface Action { - type: string - payload?: Payload + type: string; + payload?: Payload; } export function createSymbiote( @@ -28,4 +30,4 @@ export function createSymbiote( ): { actions: Actions reducer: Reducer -} +}; diff --git a/types/test.ts b/types/test.ts index eef4337..dc59ad8 100644 --- a/types/test.ts +++ b/types/test.ts @@ -1,25 +1,25 @@ -import { createSymbiote } from "redux-symbiote" +import { createSymbiote } from "redux-symbiote"; // empty -;(() => { +{ // Works correct with empty types - const { actions, reducer } = createSymbiote<{}, {}>({}, {}) - actions // $ExpectType {} - reducer // $ExpectType Reducer<{}> -})() + const { actions, reducer } = createSymbiote<{}, {}>({}, {}); + actions; // $ExpectType {} + reducer; // $ExpectType Reducer<{}> +} // plain symbiote interface PlainState { - count: number + count: number; } interface PlainActions { - inc: () => number - dec: () => number + inc: () => number; + dec: () => number; } -;(() => { +{ // Works correct with plain state and actions const { actions, reducer } = createSymbiote( @@ -28,38 +28,38 @@ interface PlainActions { inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), dec: (state: PlainState) => ({ ...state, count: state.count + 1 }), }, - ) + ); - actions // $ExpectType PlainActions - reducer // $ExpectType Reducer -})() -;(() => { + actions; // $ExpectType PlainActions + reducer; // $ExpectType Reducer +} +{ // Throw error if actions config have a incorrect type const symbiotes = { inc: (state: PlainState) => ({ ...state, count: "hello!" }), dec: (state: PlainState) => ({ ...state, count: state.count - 1 }), - } + }; - createSymbiote({ count: 0 }, symbiotes) // $ExpectError -})() -;(() => { + createSymbiote({ count: 0 }, symbiotes); // $ExpectError +} +{ // Throw error if initial state have a incorrect type const symbiotes = { inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), dec: (state: PlainState) => ({ ...state, count: state.count + 1 }), - } + }; - createSymbiote({ count: "hello!" }, symbiotes) // $ExpectError -})() + createSymbiote({ count: "hello!" }, symbiotes); // $ExpectError +} // symbiote with arguments interface ArgumentedActions { - change: (diff: number) => number + change: (diff: number) => number; } -;(() => { +{ // Works correct with plain state and actions with argument const { actions, reducer } = createSymbiote( @@ -70,12 +70,12 @@ interface ArgumentedActions { count: state.count + diff, }), }, - ) + ); - actions // $ExpectType ArgumentedActions - reducer // $ExpectType Reducer -})() -;(() => { + actions; // $ExpectType ArgumentedActions + reducer; // $ExpectType Reducer +} +{ // Throw error if action payload have a incorrect type // TODO: Must throw error! @@ -85,25 +85,25 @@ interface ArgumentedActions { ...state, count: state.count + parseInt(diff, 10), }), - } + }; - createSymbiote({ count: 0 }, symbiote) -})() + createSymbiote({ count: 0 }, symbiote); +} // nested symbiote interface NestedState { counter: { count: number - } + }; } interface NestedActions { counter: { inc: () => number - } + }; } -;(() => { +{ // Works correct with nested state and actions const { actions, reducer } = createSymbiote( @@ -116,12 +116,12 @@ interface NestedActions { }), }, }, - ) + ); - actions // $ExpectType NestedActions - reducer // $ExpectType Reducer -})() -;(() => { + actions; // $ExpectType NestedActions + reducer; // $ExpectType Reducer +} +{ // Throws error if nested state have an incorrect type const symbiote = { @@ -131,17 +131,17 @@ interface NestedActions { counter: { ...state.counter, count: state.counter.count + 1 }, }), }, - } + }; const { actions, reducer } = createSymbiote( - { counter: { cnt: 0 } }, + { counter: { cnt: 0 } }, // $ExpectError symbiote, - ) // $ExpectError + ); - actions // $ExpectType NestedActions - reducer // $ExpectType Reducer -})() -;(() => { + actions; // $ExpectType NestedActions + reducer; // $ExpectType Reducer +} +{ // Throws error if nested action have an incorrect type const symbiote = { @@ -151,13 +151,13 @@ interface NestedActions { counter: { ...state.counter, count: "newString" }, }), }, - } + }; const { actions, reducer } = createSymbiote( { counter: { count: 0 } }, - symbiote, - ) // $ExpectError + symbiote, // $ExpectError + ); - actions // $ExpectType NestedActions - reducer // $ExpectType Reducer -})() + actions; // $ExpectType NestedActions + reducer; // $ExpectType Reducer +} diff --git a/types/tslint.json b/types/tslint.json index 4221fb3..e4bac4c 100644 --- a/types/tslint.json +++ b/types/tslint.json @@ -2,6 +2,6 @@ "extends": "dtslint/dtslint.json", "rules": { "semicolon": [true, "always"], - "indent": [true, "tabs"] + "indent": [true, "spaces", 2] } } From 49d24374b0c304ef9d020916dbf4b17127d465df Mon Sep 17 00:00:00 2001 From: Surgie Finesse Date: Tue, 15 Oct 2019 13:15:18 +1000 Subject: [PATCH 2/4] feat: implement the automatic actions typing --- types/index.d.ts | 46 +++++++++++----- types/test.ts | 134 ++++++++++++++++++++++++++--------------------- 2 files changed, 107 insertions(+), 73 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index a82adf9..9e3761d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -4,30 +4,52 @@ export {}; export interface NamespaceOptions { namespace?: string; - defaultReducer?: (prevState: State, action: Action) => State; + defaultReducer?: Reducer; separator?: string; } -export type Symbiote = (state: State, ...payload: any[]) => State; +// todo: Use just `export type Symbiote = (state: State, ...payload: Arguments) => State;` when only TypeScript version 3+ is supported +type Symbiote0 = (state: State) => State; +type Symbiote1 = (state: State, arg1: A1) => State; +type Symbiote2 = (state: State, arg1: A1, arg2: A2) => State; +type Symbiote3 = (state: State, arg1: A1, arg2: A2, arg3: A3) => State; +type Symbiote4 = (state: State, arg1: A1, arg2: A2, arg3: A3, arg4: A4, ...args: any[]) => State; -export type Reducer = (state: State, action: Action) => State; +export type Reducer = (state: State, action: Action) => State; -export type ActionsConfig = { - [Key in keyof Actions]: Actions[Key] extends Function // tslint:disable-line - ? Symbiote - : ActionsConfig +export type Symbiotes = { + [Key in any]: + Symbiote0 | + Symbiote1 | + Symbiote2 | + Symbiote3 | + Symbiote4 | + Symbiotes; }; -export interface Action { +export type ActionsCreators> = { + [Key in keyof TSymbiotes]: + TSymbiotes[Key] extends Symbiote0 ? () => Action : + TSymbiotes[Key] extends Symbiote1 ? (arg1: A1) => Action<[A1]> : + TSymbiotes[Key] extends Symbiote2 ? (arg1: A1, arg2: A2) => Action<[A1, A2]> : + TSymbiotes[Key] extends Symbiote3 ? (arg1: A1, arg2: A2, arg3: A3) => Action<[A1, A2, A3]> : + TSymbiotes[Key] extends Symbiote4 ? (arg1: A1, arg2: A2, arg3: A3, arg4: A4, ...args: any[]) => Action<[A1, A2, A3, A4] & any[]> : + TSymbiotes[Key] extends Symbiotes ? ActionsCreators : + never +}; + +// todo: Replace [never] with [] when only TypeScript version 3+ is supported +export interface Action { type: string; - payload?: Payload; + payload: Payload[0]; + "symbiote-payload": Payload; } -export function createSymbiote( +export function createSymbiote>( initialState: State, - actionsConfig: ActionsConfig, + actionsConfig: TSymbiotes, namespaceOptions?: string | NamespaceOptions, ): { - actions: Actions + actions: ActionsCreators reducer: Reducer }; diff --git a/types/test.ts b/types/test.ts index dc59ad8..0e5842d 100644 --- a/types/test.ts +++ b/types/test.ts @@ -1,12 +1,15 @@ -import { createSymbiote } from "redux-symbiote"; +import { Action, createSymbiote, Reducer } from "redux-symbiote"; // empty { // Works correct with empty types - const { actions, reducer } = createSymbiote<{}, {}>({}, {}); - actions; // $ExpectType {} - reducer; // $ExpectType Reducer<{}> + const { actions, reducer } = createSymbiote({}, {}); + + // dtslint doesn't support duck typing so the following line emits an error + // actions; // $ExpectType {} + actions as {}; + reducer as Reducer<{}>; } // plain symbiote @@ -14,15 +17,15 @@ interface PlainState { count: number; } -interface PlainActions { - inc: () => number; - dec: () => number; +interface PlainActionCreators { + inc: () => Action; + dec: () => Action; } { // Works correct with plain state and actions - const { actions, reducer } = createSymbiote( + const { actions, reducer } = createSymbiote( { count: 0 }, { inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), @@ -30,64 +33,77 @@ interface PlainActions { }, ); - actions; // $ExpectType PlainActions - reducer; // $ExpectType Reducer + actions as PlainActionCreators; + reducer as Reducer; + + actions.inc(); + actions.dec(); } { - // Throw error if actions config have a incorrect type + // Throw error if the initial state type doesn't match the symbiotes state type const symbiotes = { - inc: (state: PlainState) => ({ ...state, count: "hello!" }), - dec: (state: PlainState) => ({ ...state, count: state.count - 1 }), + inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), + dec: (state: PlainState) => ({ ...state, count: state.count + 1 }), }; - createSymbiote({ count: 0 }, symbiotes); // $ExpectError + createSymbiote({ count: "hello!" }, symbiotes); // $ExpectError } { - // Throw error if initial state have a incorrect type + // Throw error if a symbiote return state type doesn't match the initial state type const symbiotes = { - inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), - dec: (state: PlainState) => ({ ...state, count: state.count + 1 }), + inc: (state: PlainState) => ({ ...state, count: 'inc' }), + dec: (state: PlainState) => ({ ...state, count: 'dec' }), }; - createSymbiote({ count: "hello!" }, symbiotes); // $ExpectError + createSymbiote({ count: 0 }, symbiotes); // $ExpectError } -// symbiote with arguments -interface ArgumentedActions { - change: (diff: number) => number; +// symbiotes with arguments +interface ArgumentedActionCreators { + oneArg: (one: number) => Action<[number]>; + twoArgs: (one: string, two: number) => Action<[string, number]>; + threeArgs: (one: boolean, two: number, three: string) => Action<[boolean, number, string]>; + manyArgs: (one: '1', two: '2', three: '3', four: '4', five: '5', six: '6') => Action<['1', '2', '3', '4'] & any[]>; } +const argumentedSymbiotes = { + oneArg: (state: PlainState, one: number) => state, + twoArgs: (state: PlainState, one: string, two: number) => state, + threeArgs: (state: PlainState, one: boolean, two: number, three: string) => state, + manyArgs: (state: PlainState, one: '1', two: '2', three: '3', four: '4', five: '5', six: '6') => state, +}; + { // Works correct with plain state and actions with argument - const { actions, reducer } = createSymbiote( + const { actions, reducer } = createSymbiote( { count: 0 }, - { - change: (state: PlainState, diff: number) => ({ - ...state, - count: state.count + diff, - }), - }, + argumentedSymbiotes, ); - actions; // $ExpectType ArgumentedActions - reducer; // $ExpectType Reducer + actions as ArgumentedActionCreators; + reducer as Reducer; + + actions.manyArgs('1', '2', '3', '4', '5', '6'); } { - // Throw error if action payload have a incorrect type - - // TODO: Must throw error! - - const symbiote = { - change: (state: PlainState, diff: string) => ({ - ...state, - count: state.count + parseInt(diff, 10), - }), - }; - - createSymbiote({ count: 0 }, symbiote); + // Throw error if an action payload has an incorrect type + + const { actions } = createSymbiote({ count: 0 }, argumentedSymbiotes); + + actions.oneArg(); // $ExpectError + actions.oneArg('wrong'); // $ExpectError + actions.oneArg(1, 'excess'); // $ExpectError + actions.twoArgs('too few'); // $ExpectError + actions.twoArgs(1, 'wrong'); // $ExpectError + actions.twoArgs('right', 1, 'excess'); // $ExpectError + actions.threeArgs(true, 1); // $ExpectError + actions.threeArgs('wrong', 'wrong', true); // $ExpectError + actions.threeArgs(true, 1, 'right', 'excess'); // $ExpectError + actions.manyArgs('1', '2', '3'); // $ExpectError + actions.manyArgs('wrong', 1, 1, 1, 1, 1); // $ExpectError } // nested symbiote @@ -97,29 +113,31 @@ interface NestedState { }; } -interface NestedActions { +interface NestedActionCreators { counter: { - inc: () => number + inc: (amount: number) => Action<[number]> }; } { // Works correct with nested state and actions - const { actions, reducer } = createSymbiote( + const { actions, reducer } = createSymbiote( { counter: { count: 0 } }, { counter: { - inc: (state: NestedState) => ({ + inc: (state: NestedState, amount: number) => ({ ...state, - counter: { ...state.counter, count: state.counter.count + 1 }, + counter: { ...state.counter, count: state.counter.count + amount }, }), }, }, ); - actions; // $ExpectType NestedActions - reducer; // $ExpectType Reducer + actions as NestedActionCreators; + reducer as Reducer; + + actions.counter.inc(1); } { // Throws error if nested state have an incorrect type @@ -133,13 +151,10 @@ interface NestedActions { }, }; - const { actions, reducer } = createSymbiote( - { counter: { cnt: 0 } }, // $ExpectError - symbiote, - ); + const { actions, reducer } = createSymbiote({ counter: { cnt: 0 } }, symbiote); // $ExpectError - actions; // $ExpectType NestedActions - reducer; // $ExpectType Reducer + actions as NestedActionCreators; + reducer as Reducer; } { // Throws error if nested action have an incorrect type @@ -153,11 +168,8 @@ interface NestedActions { }, }; - const { actions, reducer } = createSymbiote( - { counter: { count: 0 } }, - symbiote, // $ExpectError - ); + const { actions, reducer } = createSymbiote({ counter: { count: 0 } }, symbiote); // $ExpectError - actions; // $ExpectType NestedActions - reducer; // $ExpectType Reducer + actions as NestedActionCreators; + reducer as Reducer; } From daacb4353de367bf1e961fbf43480706c86fe686 Mon Sep 17 00:00:00 2001 From: Surgie Finesse Date: Tue, 15 Oct 2019 13:49:54 +1000 Subject: [PATCH 3/4] feat: optional symbiote arguments support --- types/index.d.ts | 34 +++++++++++----------------------- types/test.ts | 26 +++++++++++--------------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 9e3761d..3a13c40 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,4 +1,4 @@ -// TypeScript Version: 2.8 +// TypeScript Version: 3.0 export {}; @@ -8,38 +8,26 @@ export interface NamespaceOptions { separator?: string; } -// todo: Use just `export type Symbiote = (state: State, ...payload: Arguments) => State;` when only TypeScript version 3+ is supported -type Symbiote0 = (state: State) => State; -type Symbiote1 = (state: State, arg1: A1) => State; -type Symbiote2 = (state: State, arg1: A1, arg2: A2) => State; -type Symbiote3 = (state: State, arg1: A1, arg2: A2, arg3: A3) => State; -type Symbiote4 = (state: State, arg1: A1, arg2: A2, arg3: A3, arg4: A4, ...args: any[]) => State; - -export type Reducer = (state: State, action: Action) => State; +export type Symbiote = (state: State, ...payload: Arguments) => State; export type Symbiotes = { - [Key in any]: - Symbiote0 | - Symbiote1 | - Symbiote2 | - Symbiote3 | - Symbiote4 | - Symbiotes; + [Key in any]: Symbiote | Symbiotes; }; +export type Reducer = (state: State, action: Action) => State; + +export type ActionCreator = TSymbiote extends Symbiote + ? (...payload: Arguments) => Action + : never; + export type ActionsCreators> = { [Key in keyof TSymbiotes]: - TSymbiotes[Key] extends Symbiote0 ? () => Action : - TSymbiotes[Key] extends Symbiote1 ? (arg1: A1) => Action<[A1]> : - TSymbiotes[Key] extends Symbiote2 ? (arg1: A1, arg2: A2) => Action<[A1, A2]> : - TSymbiotes[Key] extends Symbiote3 ? (arg1: A1, arg2: A2, arg3: A3) => Action<[A1, A2, A3]> : - TSymbiotes[Key] extends Symbiote4 ? (arg1: A1, arg2: A2, arg3: A3, arg4: A4, ...args: any[]) => Action<[A1, A2, A3, A4] & any[]> : + TSymbiotes[Key] extends Symbiote ? ActionCreator : TSymbiotes[Key] extends Symbiotes ? ActionsCreators : never }; -// todo: Replace [never] with [] when only TypeScript version 3+ is supported -export interface Action { +export interface Action { type: string; payload: Payload[0]; "symbiote-payload": Payload; diff --git a/types/test.ts b/types/test.ts index 0e5842d..bc02539 100644 --- a/types/test.ts +++ b/types/test.ts @@ -63,16 +63,14 @@ interface PlainActionCreators { // symbiotes with arguments interface ArgumentedActionCreators { oneArg: (one: number) => Action<[number]>; - twoArgs: (one: string, two: number) => Action<[string, number]>; - threeArgs: (one: boolean, two: number, three: string) => Action<[boolean, number, string]>; - manyArgs: (one: '1', two: '2', three: '3', four: '4', five: '5', six: '6') => Action<['1', '2', '3', '4'] & any[]>; + oneOptionalArg: (one?: number) => Action<[number | undefined]>; + manyArgs: (one: number, two: boolean, three: string) => Action<[number, boolean, string]>; } const argumentedSymbiotes = { oneArg: (state: PlainState, one: number) => state, - twoArgs: (state: PlainState, one: string, two: number) => state, - threeArgs: (state: PlainState, one: boolean, two: number, three: string) => state, - manyArgs: (state: PlainState, one: '1', two: '2', three: '3', four: '4', five: '5', six: '6') => state, + oneOptionalArg: (state: PlainState, one?: number) => state, + manyArgs: (state: PlainState, one: number, two: boolean, three: string) => state, }; { @@ -86,7 +84,10 @@ const argumentedSymbiotes = { actions as ArgumentedActionCreators; reducer as Reducer; - actions.manyArgs('1', '2', '3', '4', '5', '6'); + actions.oneArg(1); + actions.oneOptionalArg(); + actions.oneOptionalArg(1); + actions.manyArgs(1, true, 'str'); } { // Throw error if an action payload has an incorrect type @@ -96,14 +97,9 @@ const argumentedSymbiotes = { actions.oneArg(); // $ExpectError actions.oneArg('wrong'); // $ExpectError actions.oneArg(1, 'excess'); // $ExpectError - actions.twoArgs('too few'); // $ExpectError - actions.twoArgs(1, 'wrong'); // $ExpectError - actions.twoArgs('right', 1, 'excess'); // $ExpectError - actions.threeArgs(true, 1); // $ExpectError - actions.threeArgs('wrong', 'wrong', true); // $ExpectError - actions.threeArgs(true, 1, 'right', 'excess'); // $ExpectError - actions.manyArgs('1', '2', '3'); // $ExpectError - actions.manyArgs('wrong', 1, 1, 1, 1, 1); // $ExpectError + actions.manyArgs(1, true); // $ExpectError + actions.manyArgs('wrong', 'wrong', true); // $ExpectError + actions.manyArgs(1, true, 'str', 'excess'); // $ExpectError } // nested symbiote From bce8d1db4a7fcffaa06c7cab08c1001128f9aaa8 Mon Sep 17 00:00:00 2001 From: Surgie Finesse Date: Tue, 15 Oct 2019 16:06:44 +1000 Subject: [PATCH 4/4] refactor: amend the reducer type --- types/index.d.ts | 6 +++++- types/test.ts | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index 3a13c40..4db14f9 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -14,7 +14,11 @@ export type Symbiotes = { [Key in any]: Symbiote | Symbiotes; }; -export type Reducer = (state: State, action: Action) => State; +interface BasicAction { + type: string | number | symbol; +} + +export type Reducer = (state: State, action: BasicAction) => State; export type ActionCreator = TSymbiote extends Symbiote ? (...payload: Arguments) => Action diff --git a/types/test.ts b/types/test.ts index bc02539..6b77c35 100644 --- a/types/test.ts +++ b/types/test.ts @@ -25,8 +25,9 @@ interface PlainActionCreators { { // Works correct with plain state and actions + const initialState = { count: 0 }; const { actions, reducer } = createSymbiote( - { count: 0 }, + initialState, { inc: (state: PlainState) => ({ ...state, count: state.count + 1 }), dec: (state: PlainState) => ({ ...state, count: state.count + 1 }), @@ -37,7 +38,8 @@ interface PlainActionCreators { reducer as Reducer; actions.inc(); - actions.dec(); + reducer(initialState, actions.dec()); + reducer(initialState, { type: 'other' }); } { // Throw error if the initial state type doesn't match the symbiotes state type