From c6eefd15eff8122d61cfe96fca4e6b60cfa224ad Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Sun, 28 Jul 2019 14:57:04 -0700 Subject: [PATCH 1/4] Add type overload for combineReducers which strictly infers state shape and actions from the reducers object map. --- index.d.ts | 41 ++++++++++++++++++++ package.json | 2 +- test/typescript/reducers.ts | 75 ++++++++++++++++++++++++++++--------- 3 files changed, 99 insertions(+), 19 deletions(-) diff --git a/index.d.ts b/index.d.ts index c1a16080bf..c7ae6854ed 100644 --- a/index.d.ts +++ b/index.d.ts @@ -72,6 +72,44 @@ export type ReducersMapObject = { [K in keyof S]: Reducer } +/** + * Infer a combined state shape from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type StateFromReducersMapObject = M extends ReducersMapObject + ? { [P in keyof M]: M[P] extends Reducer ? S : never } + : never + +/** + * Infer reducer union type from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type ReducerFromReducersMapObject = M extends { + [P in keyof M]: infer R +} + ? R extends Reducer + ? R + : never + : never + +/** + * Infer action type from a reducer function. + * + * @template R Type of reducer. + */ +export type ActionFromReducer = R extends Reducer ? A : never + +/** + * Infer action union type from a `ReducersMapObject`. + * + * @template M Object map of reducers as provided to `combineReducers(map: M)`. + */ +export type ActionFromReducersMapObject = M extends ReducersMapObject + ? ActionFromReducer> + : never + /** * Turns an object whose values are different reducer functions, into a single * reducer function. It will call every child reducer, and gather their results @@ -96,6 +134,9 @@ export function combineReducers( export function combineReducers( reducers: ReducersMapObject ): Reducer +export function combineReducers>( + reducers: O +): Reducer, ActionFromReducersMapObject> /* store */ diff --git a/package.json b/package.json index 6dc69e8d77..d8bafaa9d8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux", - "version": "4.0.4", + "version": "4.0.5", "description": "Predictable state container for JavaScript apps", "license": "MIT", "homepage": "http://redux.js.org", diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index 29376714f3..e515bb1efd 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -68,10 +68,21 @@ function discriminated() { count?: number } + interface MultiplyAction { + type: 'MULTIPLY' + count?: number + } + + interface DivideAction { + type: 'DIVIDE' + count?: number + } + // Union of all actions in the app. - type MyAction = IncrementAction | DecrementAction + type MyAction0 = IncrementAction | DecrementAction + type MyOther1 = MultiplyAction | DivideAction - const reducer: Reducer = (state = 0, action) => { + const reducer0: Reducer = (state = 0, action) => { if (action.type === 'INCREMENT') { // Action shape is determined by `type` discriminator. // typings:expect-error @@ -94,37 +105,65 @@ function discriminated() { return state } + const reducer1: Reducer = (state = 0, action) => { + if (action.type === 'MULTIPLY') { + // typings:expect-error + action.wrongField + + const { count = 1 } = action + + return state * count + } + + if (action.type === 'DIVIDE') { + // typings:expect-error + action.wrongField + + const { count = 1 } = action + + return state / count + } + + return state + } + // Reducer state is initialized by Redux using Init action which is private. // To initialize manually (e.g. in tests) we have to type cast init action // or add a custom init action to MyAction union. - let s: State = reducer(undefined, { type: 'init' } as any) - s = reducer(s, { type: 'INCREMENT' }) - s = reducer(s, { type: 'INCREMENT', count: 10 }) + let s: State = reducer0(undefined, { type: 'init' } as any) + s = reducer0(s, { type: 'INCREMENT' }) + s = reducer0(s, { type: 'INCREMENT', count: 10 }) // Known actions are strictly checked. // typings:expect-error - s = reducer(s, { type: 'DECREMENT', coun: 10 }) - s = reducer(s, { type: 'DECREMENT', count: 10 }) + s = reducer0(s, { type: 'DECREMENT', coun: 10 }) + s = reducer0(s, { type: 'DECREMENT', count: 10 }) // Unknown actions are rejected. // typings:expect-error - s = reducer(s, { type: 'SOME_OTHER_TYPE' }) + s = reducer0(s, { type: 'SOME_OTHER_TYPE' }) // typings:expect-error - s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' }) + s = reducer0(s, { type: 'SOME_OTHER_TYPE', someField: 'value' }) - // Combined reducer accepts any action by default which allows to include - // third-party reducers without the need to add their actions to the union. - const combined = combineReducers({ sub: reducer }) + // Combined reducer infers state and actions by default which maintains type + // safety and still allows inclusion of third-party reducers without the need + // to explicitly add their state and actions to the union. + const combined = combineReducers({ sub0: reducer0, sub1: reducer1 }) - let cs: { sub: State } = combined(undefined, { type: 'init' }) - cs = combined(cs, { type: 'SOME_OTHER_TYPE' }) + const cs = combined(undefined, { type: 'INCREMENT' }) + combined(cs, { type: 'MULTIPLY' }) + // typings:expect-error + combined(cs, { type: 'init' }) + // typings:expect-error + combined(cs, { type: 'SOME_OTHER_TYPE' }) // Combined reducer can be made to only accept known actions. - const strictCombined = combineReducers<{ sub: State }, MyAction>({ - sub: reducer + const strictCombined = combineReducers<{ sub: State }, MyAction0>({ + sub: reducer0 }) - strictCombined(cs, { type: 'INCREMENT' }) + const scs = strictCombined(undefined, { type: 'INCREMENT' }) + strictCombined(scs, { type: 'DECREMENT' }) // typings:expect-error - strictCombined(cs, { type: 'SOME_OTHER_TYPE' }) + strictCombined(scs, { type: 'SOME_OTHER_TYPE' }) } /** From d8d0d834f630bced5d6c3cd7849d8f0a3352e6df Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Sun, 28 Jul 2019 15:03:33 -0700 Subject: [PATCH 2/4] Fixed some typos. --- index.d.ts | 6 +++--- test/typescript/reducers.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/index.d.ts b/index.d.ts index c7ae6854ed..813620057f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -134,9 +134,9 @@ export function combineReducers( export function combineReducers( reducers: ReducersMapObject ): Reducer -export function combineReducers>( - reducers: O -): Reducer, ActionFromReducersMapObject> +export function combineReducers>( + reducers: M +): Reducer, ActionFromReducersMapObject> /* store */ diff --git a/test/typescript/reducers.ts b/test/typescript/reducers.ts index e515bb1efd..5a109eeece 100644 --- a/test/typescript/reducers.ts +++ b/test/typescript/reducers.ts @@ -80,7 +80,7 @@ function discriminated() { // Union of all actions in the app. type MyAction0 = IncrementAction | DecrementAction - type MyOther1 = MultiplyAction | DivideAction + type MyAction1 = MultiplyAction | DivideAction const reducer0: Reducer = (state = 0, action) => { if (action.type === 'INCREMENT') { @@ -105,7 +105,7 @@ function discriminated() { return state } - const reducer1: Reducer = (state = 0, action) => { + const reducer1: Reducer = (state = 0, action) => { if (action.type === 'MULTIPLY') { // typings:expect-error action.wrongField From 68b525240e7a03339f5fd2fc595eb3a3df146eb2 Mon Sep 17 00:00:00 2001 From: Tim Dorr Date: Sun, 28 Jul 2019 21:11:20 -0400 Subject: [PATCH 3/4] Please don't change version numbers in a PR --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d8bafaa9d8..6dc69e8d77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "redux", - "version": "4.0.5", + "version": "4.0.4", "description": "Predictable state container for JavaScript apps", "license": "MIT", "homepage": "http://redux.js.org", From e26cb190e80250ccb659e92d25076b23ae45e81c Mon Sep 17 00:00:00 2001 From: Chris Ackerman Date: Mon, 29 Jul 2019 13:16:14 -0700 Subject: [PATCH 4/4] Typescript 2.8 default type fixes. --- index.d.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/index.d.ts b/index.d.ts index 813620057f..c9c5a57d75 100644 --- a/index.d.ts +++ b/index.d.ts @@ -77,7 +77,10 @@ export type ReducersMapObject = { * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type StateFromReducersMapObject = M extends ReducersMapObject +export type StateFromReducersMapObject = M extends ReducersMapObject< + any, + any +> ? { [P in keyof M]: M[P] extends Reducer ? S : never } : never @@ -106,7 +109,10 @@ export type ActionFromReducer = R extends Reducer ? A : never * * @template M Object map of reducers as provided to `combineReducers(map: M)`. */ -export type ActionFromReducersMapObject = M extends ReducersMapObject +export type ActionFromReducersMapObject = M extends ReducersMapObject< + any, + any +> ? ActionFromReducer> : never