Skip to content

Commit 6de0dd0

Browse files
committed
alternative callback-builder-style notation for actionsMap
1 parent ca5d6cf commit 6de0dd0

File tree

5 files changed

+188
-3
lines changed

5 files changed

+188
-3
lines changed

src/createReducer.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createReducer, CaseReducer } from './createReducer'
2-
import { PayloadAction } from './createAction'
2+
import { PayloadAction, createAction } from './createAction'
33
import { Reducer } from 'redux'
44

55
interface Todo {
@@ -74,6 +74,73 @@ describe('createReducer', () => {
7474

7575
behavesLikeReducer(todosReducer)
7676
})
77+
78+
describe('alternative builder callback for actionMap', () => {
79+
const increment = createAction<number, 'increment'>('increment')
80+
const decrement = createAction<number, 'decrement'>('decrement')
81+
82+
test('can be used with ActionCreators', () => {
83+
const reducer = createReducer(0, builder =>
84+
builder
85+
.addCase(increment, (state, action) => state + action.payload)
86+
.addCase(decrement, (state, action) => state - action.payload)
87+
)
88+
expect(reducer(0, increment(5))).toBe(5)
89+
expect(reducer(5, decrement(5))).toBe(0)
90+
})
91+
test('can be used with string types', () => {
92+
const reducer = createReducer(0, builder =>
93+
builder
94+
.addCase(
95+
'increment',
96+
(state, action: { type: 'increment'; payload: number }) =>
97+
state + action.payload
98+
)
99+
.addCase(
100+
'decrement',
101+
(state, action: { type: 'decrement'; payload: number }) =>
102+
state - action.payload
103+
)
104+
)
105+
expect(reducer(0, increment(5))).toBe(5)
106+
expect(reducer(5, decrement(5))).toBe(0)
107+
})
108+
test('can be used with ActionCreators and string types combined', () => {
109+
const reducer = createReducer(0, builder =>
110+
builder
111+
.addCase(increment, (state, action) => state + action.payload)
112+
.addCase(
113+
'decrement',
114+
(state, action: { type: 'decrement'; payload: number }) =>
115+
state - action.payload
116+
)
117+
)
118+
expect(reducer(0, increment(5))).toBe(5)
119+
expect(reducer(5, decrement(5))).toBe(0)
120+
})
121+
test('will throw if the same type is used twice', () => {
122+
expect(() =>
123+
createReducer(0, builder =>
124+
builder
125+
.addCase(increment, (state, action) => state + action.payload)
126+
.addCase(increment, (state, action) => state + action.payload)
127+
.addCase(decrement, (state, action) => state - action.payload)
128+
)
129+
).toThrowErrorMatchingInlineSnapshot(
130+
`"addCase cannot be called with two reducers for the same action type"`
131+
)
132+
expect(() =>
133+
createReducer(0, builder =>
134+
builder
135+
.addCase(increment, (state, action) => state + action.payload)
136+
.addCase('increment', state => state + 1)
137+
.addCase(decrement, (state, action) => state - action.payload)
138+
)
139+
).toThrowErrorMatchingInlineSnapshot(
140+
`"addCase cannot be called with two reducers for the same action type"`
141+
)
142+
})
143+
})
77144
})
78145

79146
function behavesLikeReducer(todosReducer: TodosReducer) {

src/createReducer.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import createNextState, { Draft } from 'immer'
22
import { AnyAction, Action, Reducer } from 'redux'
3+
import {
4+
executeReducerBuilderCallback,
5+
ActionReducerMapBuilder
6+
} from './mapBuilders'
37

48
/**
59
* Defines a mapping from action types to corresponding action object shapes.
@@ -51,7 +55,24 @@ export type CaseReducers<S, AS extends Actions> = {
5155
export function createReducer<
5256
S,
5357
CR extends CaseReducers<S, any> = CaseReducers<S, any>
54-
>(initialState: S, actionsMap: CR): Reducer<S> {
58+
>(initialState: S, actionsMap: CR): Reducer<S>
59+
60+
export function createReducer<S>(
61+
initialState: S,
62+
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
63+
): Reducer<S>
64+
65+
export function createReducer<S>(
66+
initialState: S,
67+
mapOrBuilderCallback:
68+
| CaseReducers<S, any>
69+
| ((builder: ActionReducerMapBuilder<S>) => void)
70+
): Reducer<S> {
71+
let actionsMap =
72+
typeof mapOrBuilderCallback === 'function'
73+
? executeReducerBuilderCallback(mapOrBuilderCallback)
74+
: mapOrBuilderCallback
75+
5576
return function(state = initialState, action): S {
5677
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
5778
// than an Immutable<S>, and TypeScript cannot find out how to reconcile

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './createReducer'
88
export * from './createSlice'
99
export * from './serializableStateInvariantMiddleware'
1010
export * from './getDefaultMiddleware'
11+
export { ActionReducerMapBuilder } from './mapBuilders'

src/mapBuilders.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Action } from 'redux'
2+
import { CaseReducer, CaseReducers } from './createReducer'
3+
4+
export interface TypedActionCreator<Type extends string> {
5+
(...args: any[]): Action<Type>
6+
type: Type
7+
}
8+
9+
export interface ActionReducerMapBuilder<State> {
10+
addCase<ActionCreator extends TypedActionCreator<string>>(
11+
actionCreator: ActionCreator,
12+
reducer: CaseReducer<State, ReturnType<ActionCreator>>
13+
): ActionReducerMapBuilder<State>
14+
addCase<Type extends string, A extends Action<Type>>(
15+
type: Type,
16+
reducer: CaseReducer<State, A>
17+
): ActionReducerMapBuilder<State>
18+
}
19+
20+
export function executeReducerBuilderCallback<S>(
21+
builderCallback: (builder: ActionReducerMapBuilder<S>) => void
22+
): CaseReducers<S, any> {
23+
const actionsMap: CaseReducers<S, any> = {}
24+
const builder = {
25+
addCase(
26+
typeOrActionCreator: string | TypedActionCreator<any>,
27+
reducer: CaseReducer<S>
28+
) {
29+
const type =
30+
typeof typeOrActionCreator === 'string'
31+
? typeOrActionCreator
32+
: typeOrActionCreator.type
33+
if (type in actionsMap) {
34+
throw new Error(
35+
'addCase cannot be called with two reducers for the same action type'
36+
)
37+
}
38+
actionsMap[type] = reducer
39+
return builder
40+
}
41+
}
42+
builderCallback(builder)
43+
return actionsMap
44+
}

type-tests/files/createReducer.typetest.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Reducer } from 'redux'
2-
import { createReducer } from '../../src'
2+
import { createReducer, createAction } from '../../src'
33

44
function expectType<T>(p: T) {}
55

@@ -63,3 +63,55 @@ function expectType<T>(p: T) {}
6363
}
6464
})
6565
}
66+
67+
/** Test: alternative builder callback for actionMap */
68+
{
69+
const increment = createAction<number, 'increment'>('increment')
70+
const decrement = createAction<number, 'decrement'>('decrement')
71+
72+
const reducer = createReducer(0, builder => {
73+
builder.addCase(increment, (state, action) => {
74+
expectType<number>(state)
75+
expectType<{ type: 'increment'; payload: number }>(action)
76+
// typings:expect-error
77+
expectType<string>(state)
78+
// typings:expect-error
79+
expectType<{ type: 'increment'; payload: string }>(action)
80+
// typings:expect-error
81+
expectType<{ type: 'decrement'; payload: number }>(action)
82+
})
83+
84+
builder.addCase('increment', (state, action) => {
85+
expectType<number>(state)
86+
expectType<{ type: 'increment' }>(action)
87+
// typings:expect-error
88+
expectType<{ type: 'decrement' }>(action)
89+
// typings:expect-error - this cannot be inferred and has to be manually specified
90+
expectType<{ type: 'increment'; payload: number }>(action)
91+
})
92+
93+
builder.addCase(
94+
increment,
95+
(state, action: ReturnType<typeof increment>) => state
96+
)
97+
// typings:expect-error
98+
builder.addCase(
99+
increment,
100+
(state, action: ReturnType<typeof decrement>) => state
101+
)
102+
103+
builder.addCase(
104+
'increment',
105+
(state, action: ReturnType<typeof increment>) => state
106+
)
107+
// typings:expect-error
108+
builder.addCase(
109+
'decrement',
110+
(state, action: ReturnType<typeof increment>) => state
111+
)
112+
})
113+
114+
expectType<number>(reducer(0, increment(5)))
115+
// typings:expect-error
116+
expectType<string>(reducer(0, increment(5)))
117+
}

0 commit comments

Comments
 (0)