Skip to content

Commit 63dda81

Browse files
Shakeskeyboardetimdorr
authored andcommitted
#2979 Add strict type inference overload for combineReducers. (#3484)
* Add type overload for combineReducers which strictly infers state shape and actions from the reducers object map. * Fixed some typos. * Please don't change version numbers in a PR * Typescript 2.8 default type fixes.
1 parent 6afef6a commit 63dda81

File tree

2 files changed

+104
-18
lines changed

2 files changed

+104
-18
lines changed

index.d.ts

+47
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,50 @@ export type ReducersMapObject<S = any, A extends Action = Action> = {
7272
[K in keyof S]: Reducer<S[K], A>
7373
}
7474

75+
/**
76+
* Infer a combined state shape from a `ReducersMapObject`.
77+
*
78+
* @template M Object map of reducers as provided to `combineReducers(map: M)`.
79+
*/
80+
export type StateFromReducersMapObject<M> = M extends ReducersMapObject<
81+
any,
82+
any
83+
>
84+
? { [P in keyof M]: M[P] extends Reducer<infer S, any> ? S : never }
85+
: never
86+
87+
/**
88+
* Infer reducer union type from a `ReducersMapObject`.
89+
*
90+
* @template M Object map of reducers as provided to `combineReducers(map: M)`.
91+
*/
92+
export type ReducerFromReducersMapObject<M> = M extends {
93+
[P in keyof M]: infer R
94+
}
95+
? R extends Reducer<any, any>
96+
? R
97+
: never
98+
: never
99+
100+
/**
101+
* Infer action type from a reducer function.
102+
*
103+
* @template R Type of reducer.
104+
*/
105+
export type ActionFromReducer<R> = R extends Reducer<any, infer A> ? A : never
106+
107+
/**
108+
* Infer action union type from a `ReducersMapObject`.
109+
*
110+
* @template M Object map of reducers as provided to `combineReducers(map: M)`.
111+
*/
112+
export type ActionFromReducersMapObject<M> = M extends ReducersMapObject<
113+
any,
114+
any
115+
>
116+
? ActionFromReducer<ReducerFromReducersMapObject<M>>
117+
: never
118+
75119
/**
76120
* Turns an object whose values are different reducer functions, into a single
77121
* reducer function. It will call every child reducer, and gather their results
@@ -96,6 +140,9 @@ export function combineReducers<S>(
96140
export function combineReducers<S, A extends Action = AnyAction>(
97141
reducers: ReducersMapObject<S, A>
98142
): Reducer<S, A>
143+
export function combineReducers<M extends ReducersMapObject<any, any>>(
144+
reducers: M
145+
): Reducer<StateFromReducersMapObject<M>, ActionFromReducersMapObject<M>>
99146

100147
/* store */
101148

test/typescript/reducers.ts

+57-18
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,21 @@ function discriminated() {
6868
count?: number
6969
}
7070

71+
interface MultiplyAction {
72+
type: 'MULTIPLY'
73+
count?: number
74+
}
75+
76+
interface DivideAction {
77+
type: 'DIVIDE'
78+
count?: number
79+
}
80+
7181
// Union of all actions in the app.
72-
type MyAction = IncrementAction | DecrementAction
82+
type MyAction0 = IncrementAction | DecrementAction
83+
type MyAction1 = MultiplyAction | DivideAction
7384

74-
const reducer: Reducer<State, MyAction> = (state = 0, action) => {
85+
const reducer0: Reducer<State, MyAction0> = (state = 0, action) => {
7586
if (action.type === 'INCREMENT') {
7687
// Action shape is determined by `type` discriminator.
7788
// typings:expect-error
@@ -94,37 +105,65 @@ function discriminated() {
94105
return state
95106
}
96107

108+
const reducer1: Reducer<State, MyAction1> = (state = 0, action) => {
109+
if (action.type === 'MULTIPLY') {
110+
// typings:expect-error
111+
action.wrongField
112+
113+
const { count = 1 } = action
114+
115+
return state * count
116+
}
117+
118+
if (action.type === 'DIVIDE') {
119+
// typings:expect-error
120+
action.wrongField
121+
122+
const { count = 1 } = action
123+
124+
return state / count
125+
}
126+
127+
return state
128+
}
129+
97130
// Reducer state is initialized by Redux using Init action which is private.
98131
// To initialize manually (e.g. in tests) we have to type cast init action
99132
// or add a custom init action to MyAction union.
100-
let s: State = reducer(undefined, { type: 'init' } as any)
101-
s = reducer(s, { type: 'INCREMENT' })
102-
s = reducer(s, { type: 'INCREMENT', count: 10 })
133+
let s: State = reducer0(undefined, { type: 'init' } as any)
134+
s = reducer0(s, { type: 'INCREMENT' })
135+
s = reducer0(s, { type: 'INCREMENT', count: 10 })
103136
// Known actions are strictly checked.
104137
// typings:expect-error
105-
s = reducer(s, { type: 'DECREMENT', coun: 10 })
106-
s = reducer(s, { type: 'DECREMENT', count: 10 })
138+
s = reducer0(s, { type: 'DECREMENT', coun: 10 })
139+
s = reducer0(s, { type: 'DECREMENT', count: 10 })
107140
// Unknown actions are rejected.
108141
// typings:expect-error
109-
s = reducer(s, { type: 'SOME_OTHER_TYPE' })
142+
s = reducer0(s, { type: 'SOME_OTHER_TYPE' })
110143
// typings:expect-error
111-
s = reducer(s, { type: 'SOME_OTHER_TYPE', someField: 'value' })
144+
s = reducer0(s, { type: 'SOME_OTHER_TYPE', someField: 'value' })
112145

113-
// Combined reducer accepts any action by default which allows to include
114-
// third-party reducers without the need to add their actions to the union.
115-
const combined = combineReducers({ sub: reducer })
146+
// Combined reducer infers state and actions by default which maintains type
147+
// safety and still allows inclusion of third-party reducers without the need
148+
// to explicitly add their state and actions to the union.
149+
const combined = combineReducers({ sub0: reducer0, sub1: reducer1 })
116150

117-
let cs: { sub: State } = combined(undefined, { type: 'init' })
118-
cs = combined(cs, { type: 'SOME_OTHER_TYPE' })
151+
const cs = combined(undefined, { type: 'INCREMENT' })
152+
combined(cs, { type: 'MULTIPLY' })
153+
// typings:expect-error
154+
combined(cs, { type: 'init' })
155+
// typings:expect-error
156+
combined(cs, { type: 'SOME_OTHER_TYPE' })
119157

120158
// Combined reducer can be made to only accept known actions.
121-
const strictCombined = combineReducers<{ sub: State }, MyAction>({
122-
sub: reducer
159+
const strictCombined = combineReducers<{ sub: State }, MyAction0>({
160+
sub: reducer0
123161
})
124162

125-
strictCombined(cs, { type: 'INCREMENT' })
163+
const scs = strictCombined(undefined, { type: 'INCREMENT' })
164+
strictCombined(scs, { type: 'DECREMENT' })
126165
// typings:expect-error
127-
strictCombined(cs, { type: 'SOME_OTHER_TYPE' })
166+
strictCombined(scs, { type: 'SOME_OTHER_TYPE' })
128167
}
129168

130169
/**

0 commit comments

Comments
 (0)