Skip to content

Commit 4da8c59

Browse files
deniswmarkerikson
authored andcommitted
Improve type inference of case reducers (#133)
* Improve type inference of case reducers Previously, the TypeScript compiler would reject case reducer maps with different incompatible PayloadAction types. The case reducers map and createReducer() / createSlice() types have now been restructured to allow for better type inference. Fixes #131 * Upgrade TypeScript and ESLint parser Upgraded TypeScript to 3.4.3 and switches to the new TypeScript ESLint parser.
1 parent 82014e0 commit 4da8c59

File tree

7 files changed

+80
-88
lines changed

7 files changed

+80
-88
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
extends: 'react-app',
3-
parser: 'typescript-eslint-parser',
3+
parser: '@typescript-eslint/parser',
44

55
rules: {
66
'jsx-a11y/href-no-hash': 'off',

package-lock.json

Lines changed: 44 additions & 43 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@types/jest": "^23.3.12",
1717
"@types/node": "^10.12.18",
1818
"@types/redux-immutable-state-invariant": "^2.1.0",
19+
"@typescript-eslint/parser": "^1.6.0",
1920
"babel-core": "7.0.0-bridge.0",
2021
"babel-eslint": "^10.0.1",
2122
"eslint": "^4.17.0",
@@ -30,8 +31,7 @@
3031
"rollup-plugin-babel": "^4.2.0",
3132
"rollup-plugin-commonjs": "^9.2.0",
3233
"rollup-plugin-node-resolve": "^4.0.0",
33-
"typescript": "^3.2.2",
34-
"typescript-eslint-parser": "eslint/typescript-eslint-parser",
34+
"typescript": "^3.4.3",
3535
"typings-tester": "^0.3.2"
3636
},
3737
"scripts": {

src/createReducer.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import createNextState, { Draft } from 'immer'
22
import { AnyAction, Action, Reducer } from 'redux'
33

4+
/**
5+
* Defines a mapping from action types to corresponding action object shapes.
6+
*/
7+
export type Actions<T extends keyof any = string> = Record<T, Action>
8+
49
/**
510
* An *case reducer* is a reducer function for a speficic action type. Case
611
* reducers can be composed to full reducers using `createReducer()`.
@@ -23,8 +28,8 @@ export type CaseReducer<S = any, A extends Action = AnyAction> = (
2328
/**
2429
* A mapping from action types to case reducers for `createReducer()`.
2530
*/
26-
export interface CaseReducersMapObject<S = any, A extends Action = AnyAction> {
27-
[actionType: string]: CaseReducer<S, A>
31+
export type CaseReducers<S, AS extends Actions> = {
32+
[T in keyof AS]: AS[T] extends Action ? CaseReducer<S, AS[T]> : void
2833
}
2934

3035
/**
@@ -43,17 +48,17 @@ export interface CaseReducersMapObject<S = any, A extends Action = AnyAction> {
4348
* @param actionsMap A mapping from action types to action-type-specific
4449
* case redeucers.
4550
*/
46-
export function createReducer<S = any, A extends Action = AnyAction>(
47-
initialState: S,
48-
actionsMap: CaseReducersMapObject<S, A>
49-
): Reducer<S> {
51+
export function createReducer<
52+
S,
53+
CR extends CaseReducers<S, any> = CaseReducers<S, any>
54+
>(initialState: S, actionsMap: CR): Reducer<S> {
5055
return function(state = initialState, action): S {
5156
// @ts-ignore createNextState() produces an Immutable<Draft<S>> rather
5257
// than an Immutable<S>, and TypeScript cannot find out how to reconcile
5358
// these two types.
5459
return createNextState(state, (draft: Draft<S>) => {
5560
const caseReducer = actionsMap[action.type]
56-
return caseReducer ? caseReducer(draft, action as A) : undefined
61+
return caseReducer ? caseReducer(draft, action) : undefined
5762
})
5863
}
5964
}

src/createSlice.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { createSlice } from './createSlice'
2-
import { createAction } from './createAction'
2+
import { createAction, PayloadAction } from './createAction'
33

44
describe('createSlice', () => {
55
describe('when slice is empty', () => {
66
const { actions, reducer, selectors } = createSlice({
77
reducers: {
88
increment: state => state + 1,
9-
multiply: (state, action) => state * action.payload
9+
multiply: (state, action: PayloadAction<number>) =>
10+
state * action.payload
1011
},
1112
initialState: 0
1213
})

src/createSlice.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
1-
import { Action, AnyAction, Reducer } from 'redux'
1+
import { Reducer } from 'redux'
22
import { createAction, PayloadAction } from './createAction'
3-
import { createReducer, CaseReducersMapObject } from './createReducer'
3+
import { createReducer, CaseReducers } from './createReducer'
44
import { createSliceSelector, createSelectorName } from './sliceSelector'
55

66
/**
77
* An action creator atttached to a slice.
88
*/
9-
export type SliceActionCreator<P> = (payload: P) => PayloadAction<P>
9+
export type SliceActionCreator<P> = P extends void
10+
? () => PayloadAction<void>
11+
: (payload: P) => PayloadAction<P>
1012

1113
export interface Slice<
1214
S = any,
13-
A extends Action = AnyAction,
1415
AP extends { [key: string]: any } = { [key: string]: any }
1516
> {
1617
/**
@@ -21,7 +22,7 @@ export interface Slice<
2122
/**
2223
* The slice's reducer.
2324
*/
24-
reducer: Reducer<S, A>
25+
reducer: Reducer<S>
2526

2627
/**
2728
* Action creators for the types of actions that are handled by the slice
@@ -43,9 +44,7 @@ export interface Slice<
4344
*/
4445
export interface CreateSliceOptions<
4546
S = any,
46-
A extends Action = AnyAction,
47-
CR extends CaseReducersMapObject<S, A> = CaseReducersMapObject<S, A>,
48-
CR2 extends CaseReducersMapObject<S, A> = CaseReducersMapObject<S, A>
47+
CR extends CaseReducers<S, any> = CaseReducers<S, any>
4948
> {
5049
/**
5150
* The slice's name. Used to namespace the generated action types and to
@@ -70,19 +69,15 @@ export interface CreateSliceOptions<
7069
* functions. These reducers should have existing action types used
7170
* as the keys, and action creators will _not_ be generated.
7271
*/
73-
extraReducers?: CR2
72+
extraReducers?: CaseReducers<S, any>
7473
}
7574

76-
type ExtractPayloads<
77-
S,
78-
A extends PayloadAction,
79-
CR extends CaseReducersMapObject<S, A>
80-
> = {
81-
[type in keyof CR]: CR[type] extends (state: S) => any
75+
type CaseReducerActionPayloads<CR extends CaseReducers<any, any>> = {
76+
[T in keyof CR]: CR[T] extends (state: any) => any
8277
? void
83-
: (CR[type] extends (state: S, action: PayloadAction<infer P>) => any
78+
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
8479
? P
85-
: never)
80+
: void)
8681
}
8782

8883
function getType(slice: string, actionKey: string): string {
@@ -97,13 +92,9 @@ function getType(slice: string, actionKey: string): string {
9792
*
9893
* The `reducer` argument is passed to `createReducer()`.
9994
*/
100-
export function createSlice<
101-
S = any,
102-
A extends PayloadAction = PayloadAction<any>,
103-
CR extends CaseReducersMapObject<S, A> = CaseReducersMapObject<S, A>
104-
>(
105-
options: CreateSliceOptions<S, A, CR>
106-
): Slice<S, A, ExtractPayloads<S, A, CR>> {
95+
export function createSlice<S, CR extends CaseReducers<S, any>>(
96+
options: CreateSliceOptions<S, CR>
97+
): Slice<S, CaseReducerActionPayloads<CR>> {
10798
const { slice = '', initialState } = options
10899
const reducers = options.reducers || {}
109100
const extraReducers = options.extraReducers || {}

type-tests/files/createReducer.typetest.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { AnyAction, createReducer, Reducer } from 'redux-starter-kit'
2323
}
2424

2525
/**
26-
* Test: createReducer() type parameters can be specified expliclity.
26+
* Test: createReducer() state type can be specified expliclity.
2727
*/
2828
{
2929
type CounterAction =
@@ -36,19 +36,13 @@ import { AnyAction, createReducer, Reducer } from 'redux-starter-kit'
3636
const decrementHandler = (state: number, action: CounterAction) =>
3737
state - action.payload
3838

39-
createReducer<number, CounterAction>(0, {
39+
createReducer<number>(0, {
4040
increment: incrementHandler,
4141
decrement: decrementHandler
4242
})
4343

4444
// typings:expect-error
45-
createReducer<string, CounterAction>(0, {
46-
increment: incrementHandler,
47-
decrement: decrementHandler
48-
})
49-
50-
// typings:expect-error
51-
createReducer<number, AnyAction>(0, {
45+
createReducer<string>(0, {
5246
increment: incrementHandler,
5347
decrement: decrementHandler
5448
})

0 commit comments

Comments
 (0)