Skip to content

Commit 85923d4

Browse files
phryneasmarkerikson
authored andcommitted
refactor typings for readability (#168)
* refactor createActions types to be more readable * refactor createSlice types to be more readable * add else comments
1 parent af6d05a commit 85923d4

File tree

2 files changed

+109
-66
lines changed

2 files changed

+109
-66
lines changed

src/createAction.ts

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,51 +6,55 @@ import { Action } from 'redux'
66
*
77
* @template P The type of the action's payload.
88
* @template T the type used for the action type.
9+
* @template M The type of the action's meta (optional)
910
*/
1011
export type PayloadAction<
1112
P = any,
1213
T extends string = string,
1314
M = void
14-
> = Action<T> & {
15-
payload: P
16-
} & ([M] extends [void] ? {} : { meta: M })
17-
18-
export type Diff<T, U> = T extends U ? never : T
15+
> = WithOptionalMeta<M, WithPayload<P, Action<T>>>;
1916

2017
export type PrepareAction<P> =
2118
| ((...args: any[]) => { payload: P })
2219
| ((...args: any[]) => { payload: P; meta: any })
2320

21+
22+
export type ActionCreatorWithPreparedPayload<PA extends PrepareAction<any> | void, T extends string = string> =
23+
WithTypeProperty<T, PA extends PrepareAction<infer P> ? (...args: Parameters<PA>) => PayloadAction<P, T, MetaOrVoid<PA>> : void>;
24+
25+
export type ActionCreatorWithOptionalPayload<P, T extends string = string> =
26+
WithTypeProperty<T, {
27+
(payload?: undefined): PayloadAction<undefined, T>
28+
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
29+
}>;
30+
31+
export type ActionCreatorWithoutPayload<T extends string = string> = WithTypeProperty<T, () => PayloadAction<undefined, T>>;
32+
33+
export type ActionCreatorWithPayload<P, T extends string = string> =
34+
WithTypeProperty<T, <PT extends P>(payload: PT) => PayloadAction<PT, T>>;
35+
2436
/**
2537
* An action creator that produces actions with a `payload` attribute.
2638
*/
2739
export type PayloadActionCreator<
2840
P = any,
2941
T extends string = string,
3042
PA extends PrepareAction<P> | void = void
31-
> = {
32-
type: T
33-
} & (PA extends (...args: any[]) => any
34-
? (ReturnType<PA> extends { meta: infer M }
35-
? (...args: Parameters<PA>) => PayloadAction<P, T, M>
36-
: (...args: Parameters<PA>) => PayloadAction<P, T>)
37-
: (/*
38-
* The `P` generic is wrapped with a single-element tuple to prevent the
39-
* conditional from being checked distributively, thus preserving unions
40-
* of contra-variant types.
41-
*/
42-
[undefined] extends [P]
43-
? {
44-
(payload?: undefined): PayloadAction<undefined, T>
45-
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
46-
}
47-
: [void] extends [P]
48-
? {
49-
(): PayloadAction<undefined, T>
50-
}
51-
: {
52-
<PT extends P>(payload: PT): PayloadAction<PT, T>
53-
}))
43+
> =
44+
IfPrepareActionMethodProvided<PA,
45+
ActionCreatorWithPreparedPayload<PA, T>,
46+
// else
47+
IfMaybeUndefined<P,
48+
ActionCreatorWithOptionalPayload<P, T>,
49+
// else
50+
IfVoid<P,
51+
ActionCreatorWithoutPayload<T>,
52+
// else
53+
ActionCreatorWithPayload<P, T>
54+
>
55+
>
56+
>
57+
;
5458

5559
/**
5660
* A utility function to create an action creator for the given action type
@@ -60,6 +64,8 @@ export type PayloadActionCreator<
6064
* allowing it to be used in reducer logic that is looking for that action type.
6165
*
6266
* @param type The action type to use for created actions.
67+
* @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
68+
* If this is given, the resulting action creator will pass it's arguments to this method to calculate payload & meta.
6369
*/
6470

6571
export function createAction<P = any, T extends string = string>(
@@ -108,3 +114,23 @@ export function getType<T extends string>(
108114
): T {
109115
return `${actionCreator}` as T
110116
}
117+
118+
// helper types for more readable typings
119+
120+
type Diff<T, U> = T extends U ? never : T
121+
122+
type WithPayload<P, T> = T & { payload: P };
123+
124+
type WithOptionalMeta<M, T> = T & ([M] extends [void] ? {} : { meta: M })
125+
126+
type WithTypeProperty<T, MergeIn> = {
127+
type: T
128+
} & MergeIn;
129+
130+
type IfPrepareActionMethodProvided<PA extends PrepareAction<any> | void, True, False> = PA extends (...args: any[]) => any ? True : False;
131+
132+
type MetaOrVoid<PA extends PrepareAction<any>> = (ReturnType<PA> extends { meta: infer M } ? M : void);
133+
134+
type IfMaybeUndefined<P, True, False> = [undefined] extends [P] ? True : False;
135+
136+
type IfVoid<P, True, False> = [void] extends [P] ? True : False;

src/createSlice.ts

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
createAction,
44
PayloadAction,
55
PayloadActionCreator,
6-
PrepareAction
6+
PrepareAction,
7+
ActionCreatorWithoutPayload,
8+
ActionCreatorWithPreparedPayload
79
} from './createAction'
810
import { createReducer, CaseReducers, CaseReducer } from './createReducer'
911
import { createSliceSelector, createSelectorName } from './sliceSelector'
@@ -16,9 +18,9 @@ import { createSliceSelector, createSelectorName } from './sliceSelector'
1618
export type SliceActionCreator<P> = PayloadActionCreator<P>
1719

1820
export interface Slice<
19-
S = any,
20-
AC extends { [key: string]: any } = { [key: string]: any }
21-
> {
21+
State = any,
22+
ActionCreators extends { [key: string]: any } = { [key: string]: any }
23+
> {
2224
/**
2325
* The slice name.
2426
*/
@@ -27,30 +29,30 @@ export interface Slice<
2729
/**
2830
* The slice's reducer.
2931
*/
30-
reducer: Reducer<S>
32+
reducer: Reducer<State>
3133

3234
/**
3335
* Action creators for the types of actions that are handled by the slice
3436
* reducer.
3537
*/
36-
actions: AC
38+
actions: ActionCreators
3739

3840
/**
3941
* Selectors for the slice reducer state. `createSlice()` inserts a single
4042
* selector that returns the entire slice state and whose name is
4143
* automatically derived from the slice name (e.g., `getCounter` for a slice
4244
* named `counter`).
4345
*/
44-
selectors: { [key: string]: (state: any) => S }
46+
selectors: { [key: string]: (state: any) => State }
4547
}
4648

4749
/**
4850
* Options for `createSlice()`.
4951
*/
5052
export interface CreateSliceOptions<
51-
S = any,
52-
CR extends SliceCaseReducers<S, any> = SliceCaseReducers<S, any>
53-
> {
53+
State = any,
54+
CR extends SliceCaseReducers<State, any> = SliceCaseReducers<State, any>
55+
> {
5456
/**
5557
* The slice's name. Used to namespace the generated action types and to
5658
* name the selector for retrieving the reducer's state.
@@ -60,7 +62,7 @@ export interface CreateSliceOptions<
6062
/**
6163
* The initial state to be returned by the slice reducer.
6264
*/
63-
initialState: S
65+
initialState: State
6466

6567
/**
6668
* A mapping from action types to action-type-specific *case reducer*
@@ -74,41 +76,54 @@ export interface CreateSliceOptions<
7476
* functions. These reducers should have existing action types used
7577
* as the keys, and action creators will _not_ be generated.
7678
*/
77-
extraReducers?: CaseReducers<S, any>
79+
extraReducers?: CaseReducers<State, any>
7880
}
7981

80-
type PayloadActions<T extends keyof any = string> = Record<T, PayloadAction>
82+
type PayloadActions<Types extends keyof any = string> = Record<Types, PayloadAction>
8183

82-
type EnhancedCaseReducer<S, A extends PayloadAction> = {
83-
reducer: CaseReducer<S, A>
84-
prepare: PrepareAction<A['payload']>
84+
type EnhancedCaseReducer<State, Action extends PayloadAction> = {
85+
reducer: CaseReducer<State, Action>
86+
prepare: PrepareAction<Action['payload']>
8587
}
8688

87-
type SliceCaseReducers<S, PA extends PayloadActions> = {
88-
[T in keyof PA]: CaseReducer<S, PA[T]> | EnhancedCaseReducer<S, PA[T]>
89+
type SliceCaseReducers<State, PA extends PayloadActions> = {
90+
[ActionType in keyof PA]: CaseReducer<State, PA[ActionType]> | EnhancedCaseReducer<State, PA[ActionType]>
8991
}
9092

91-
type CaseReducerActions<CR extends SliceCaseReducers<any, any>> = {
92-
[T in keyof CR]: CR[T] extends (state: any) => any
93-
? PayloadActionCreator<void>
94-
: (CR[T] extends (state: any, action: PayloadAction<infer P>) => any
95-
? PayloadActionCreator<P>
96-
: CR[T] extends { prepare: PrepareAction<infer P> }
97-
? PayloadActionCreator<P, string, CR[T]['prepare']>
98-
: PayloadActionCreator<void>)
93+
type IfIsReducerFunctionWithoutAction<R, True, False = never> = R extends (state: any) => any ? True : False;
94+
type IfIsEnhancedReducer<R, True, False = never> = R extends { prepare: Function } ? True : False;
95+
96+
type PayloadForReducer<R> = R extends (state: any, action: PayloadAction<infer P>) => any ? P : void;
97+
type PrepareActionForReducer<R> = R extends { prepare: infer Prepare } ? Prepare : never;
98+
99+
type CaseReducerActions<CaseReducers extends SliceCaseReducers<any, any>> = {
100+
[Type in keyof CaseReducers]:
101+
IfIsEnhancedReducer<CaseReducers[Type],
102+
ActionCreatorWithPreparedPayload<PrepareActionForReducer<CaseReducers[Type]>>,
103+
// else
104+
IfIsReducerFunctionWithoutAction<CaseReducers[Type],
105+
ActionCreatorWithoutPayload,
106+
// else
107+
PayloadActionCreator<PayloadForReducer<CaseReducers[Type]>>
108+
>
109+
>
99110
}
100111

101112
type NoInfer<T> = [T][T extends any ? 0 : never];
113+
102114
type SliceCaseReducersCheck<S, ACR> = {
103-
[P in keyof ACR] : ACR[P] extends {
104-
reducer(s:S, action?: { payload: infer O }): any
105-
} ? {
106-
prepare(...a:never[]): { payload: O }
107-
} : {
115+
[P in keyof ACR]: ACR[P] extends {
116+
reducer(s: S, action?: { payload: infer O }): any
117+
} ? {
118+
prepare(...a: never[]): { payload: O }
119+
} : {
108120

109-
}
121+
}
110122
}
111123

124+
type RestrictEnhancedReducersToMatchReducerAndPrepare<S, CR extends SliceCaseReducers<S, any>> =
125+
{ reducers: SliceCaseReducersCheck<S, NoInfer<CR>> };
126+
112127
function getType(slice: string, actionKey: string): string {
113128
return slice ? `${slice}/${actionKey}` : actionKey
114129
}
@@ -121,12 +136,14 @@ function getType(slice: string, actionKey: string): string {
121136
*
122137
* The `reducer` argument is passed to `createReducer()`.
123138
*/
124-
export function createSlice<S, CR extends SliceCaseReducers<S, any>>(
125-
options: CreateSliceOptions<S, CR> & { reducers: SliceCaseReducersCheck<S, NoInfer<CR>> }
126-
): Slice<S, CaseReducerActions<CR>>
127-
export function createSlice<S, CR extends SliceCaseReducers<S, any>>(
128-
options: CreateSliceOptions<S, CR>
129-
): Slice<S, CaseReducerActions<CR>> {
139+
export function createSlice<State, CaseReducers extends SliceCaseReducers<State, any>>(
140+
options: CreateSliceOptions<State, CaseReducers> & RestrictEnhancedReducersToMatchReducerAndPrepare<State, CaseReducers>
141+
): Slice<State, CaseReducerActions<CaseReducers>>
142+
143+
// internal definition is a little less restrictive
144+
export function createSlice<State, CaseReducers extends SliceCaseReducers<State, any>>(
145+
options: CreateSliceOptions<State, CaseReducers>
146+
): Slice<State, CaseReducerActions<CaseReducers>> {
130147
const { slice = '', initialState } = options
131148
const reducers = options.reducers || {}
132149
const extraReducers = options.extraReducers || {}

0 commit comments

Comments
 (0)