Skip to content

Commit 0f4cdf2

Browse files
phryneasmarkerikson
andcommitted
exportable case reducers type, bugfix, documentation (#290)
* wip * working state * simplify types, remove ActionForReducer * add documentation for types, remove obsolete `PayloadActions` type * add release tags and type documentation * specify all exports by hand * add documentation and release tags * add test "wrapping createSlice should be possible" * don't use self-referencing generic restriction to support TS <3.4 * update comments * CI please work * Update src/createSlice.ts Co-Authored-By: Mark Erikson <[email protected]> * typos * documentation * prettier * minor nitpick * Update usage-with-typescript.md * Update usage-with-typescript.md Co-authored-by: Mark Erikson <[email protected]>
1 parent 8d6a716 commit 0f4cdf2

14 files changed

+564
-244
lines changed

api-extractor.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -324,14 +324,9 @@
324324
"logLevel": "warning"
325325
// "addToApiReportFile": false
326326
},
327-
328-
"ae-missing-release-tag": {
329-
"logLevel": "none",
330-
"addToApiReportFile": true
331-
},
332327
"ae-forgotten-export": {
333328
"logLevel": "none",
334-
"addToApiReportFile": true
329+
"addToApiReportFile": false
335330
}
336331
//
337332
// . . .

docs/usage/usage-with-typescript.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,26 @@ createSlice({
195195
})
196196
```
197197

198+
### Defining the type of your `initialState`
199+
200+
You might have noticed that it is not a good idea to pass your `SliceState` type as a generic to `createSlice`. This is due to the fact that in almost all cases, follow-up generic parameters to `createSlice` need to be inferred, and TypeScript cannot mix explicit declaration and inference of generic types within the same "generic block".
201+
202+
Instead, you can use the construct `initialState: myInitialState as SliceState`.
203+
204+
```ts
205+
type SliceState = { state: 'loading' } | { state: 'finished'; data: string }
206+
207+
createSlice({
208+
name: 'test',
209+
initialState: { state: 'loading' } as SliceState,
210+
reducers: {
211+
// ...
212+
}
213+
})
214+
```
215+
216+
which will result in a `Slice<SliceState, ...>`.
217+
198218
### On the "type" property of slice action Reducers
199219

200220
As TS cannot combine two string literals (`slice.name` and the key of `actionMap`) into a new literal, all actionCreators created by createSlice are of type 'string'. This is usually not a problem, as these types are only rarely used as literals.
@@ -222,3 +242,62 @@ If you actually _need_ that type, unfortunately there is no other way than manua
222242
### Type safety with `extraReducers`
223243

224244
Like in `createReducer`, the `extraReducers` map object is not easy to fully type. So, like with `createReducer`, you may also use the "builder callback" approach for defining the reducer object argument. See [the `createReducer` section above](#createreducer) for an example.
245+
246+
### Wrapping `createSlice`
247+
248+
If you need to reuse reducer logic, it is common to write ["higher-order reducers"](https://redux.js.org/recipes/structuring-reducers/reusing-reducer-logic#customizing-behavior-with-higher-order-reducers) that wrap a reducer function with additional common behavior. This can be done with `createSlice` as well, but due to the complexity of the types for `createSlice`, you have to use the `SliceCaseReducers` and `ValidateSliceCaseReducers` types in a very specific way.
249+
250+
Here is an example of such a "generic" wrapped `createSlice` call:
251+
252+
```ts
253+
interface GenericState<T> {
254+
data?: T
255+
status: 'loading' | 'finished' | 'error'
256+
}
257+
258+
const createGenericSlice = <
259+
T,
260+
Reducers extends SliceCaseReducers<GenericState<T>>
261+
>({
262+
name = '',
263+
initialState,
264+
reducers
265+
}: {
266+
name: string
267+
initialState: GenericState<T>
268+
reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>
269+
}) => {
270+
return createSlice({
271+
name,
272+
initialState,
273+
reducers: {
274+
start(state) {
275+
state.status = 'loading'
276+
},
277+
/**
278+
* If you want to write to values of the state that depend on the generic
279+
* (in this case: `state.data`, which is T), you might need to specify the
280+
* State type manually here, as it defaults to `Draft<GenericState<T>>`,
281+
* which can sometimes be problematic with yet-unresolved generics.
282+
* This is a general problem when working with immer's Draft type and generics.
283+
*/
284+
success(state: GenericState<T>, action: PayloadAction<T>) {
285+
state.data = action.payload
286+
state.status = 'finished'
287+
},
288+
...reducers
289+
}
290+
})
291+
}
292+
293+
const wrappedSlice = createGenericSlice({
294+
name: 'test',
295+
initialState: { status: 'loading' } as GenericState<string>,
296+
reducers: {
297+
magic(state) {
298+
state.status = 'finished'
299+
state.data = 'hocus pocus'
300+
}
301+
}
302+
})
303+
```

etc/redux-toolkit.api.md

Lines changed: 47 additions & 110 deletions
Large diffs are not rendered by default.

src/configureStore.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,19 @@ import { getDefaultMiddleware } from './getDefaultMiddleware'
2323

2424
const IS_PRODUCTION = process.env.NODE_ENV === 'production'
2525

26+
/**
27+
* Callback function type, to be used in `ConfigureStoreOptions.enhancers`
28+
*
29+
* @public
30+
*/
2631
export type ConfigureEnhancersCallback = (
2732
defaultEnhancers: StoreEnhancer[]
2833
) => StoreEnhancer[]
2934

3035
/**
3136
* Options for `configureStore()`.
37+
*
38+
* @public
3239
*/
3340
export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
3441
/**
@@ -78,9 +85,14 @@ export interface ConfigureStoreOptions<S = any, A extends Action = AnyAction> {
7885
/**
7986
* A Redux store returned by `configureStore()`. Supports dispatching
8087
* side-effectful _thunks_ in addition to plain actions.
88+
*
89+
* @public
8190
*/
8291
export interface EnhancedStore<S = any, A extends Action = AnyAction>
8392
extends Store<S, A> {
93+
/**
94+
* @inheritdoc
95+
*/
8496
dispatch: ThunkDispatch<S, any, A>
8597
}
8698

@@ -89,6 +101,8 @@ export interface EnhancedStore<S = any, A extends Action = AnyAction>
89101
*
90102
* @param config The store configuration.
91103
* @returns A configured Redux store.
104+
*
105+
* @public
92106
*/
93107
export function configureStore<S = any, A extends Action = AnyAction>(
94108
options: ConfigureStoreOptions<S, A>

src/createAction.ts

Lines changed: 132 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { Action } from 'redux'
2-
import { IsUnknownOrNonInferrable, IfMaybeUndefined, IfVoid } from './tsHelpers'
2+
import {
3+
IsUnknownOrNonInferrable,
4+
IfMaybeUndefined,
5+
IfVoid,
6+
IsAny
7+
} from './tsHelpers'
38

49
/**
510
* An action with a string type and an associated payload. This is the
@@ -9,6 +14,8 @@ import { IsUnknownOrNonInferrable, IfMaybeUndefined, IfVoid } from './tsHelpers'
914
* @template T the type used for the action type.
1015
* @template M The type of the action's meta (optional)
1116
* @template E The type of the action's error (optional)
17+
*
18+
* @public
1219
*/
1320
export type PayloadAction<
1421
P = void,
@@ -29,12 +36,24 @@ export type PayloadAction<
2936
error: E
3037
})
3138

39+
/**
40+
* A "prepare" method to be used as the second parameter of `createAction`.
41+
* Takes any number of arguments and returns a Flux Standard Action without
42+
* type (will be added later) that *must* contain a payload (might be undefined).
43+
*
44+
* @public
45+
*/
3246
export type PrepareAction<P> =
3347
| ((...args: any[]) => { payload: P })
3448
| ((...args: any[]) => { payload: P; meta: any })
3549
| ((...args: any[]) => { payload: P; error: any })
3650
| ((...args: any[]) => { payload: P; meta: any; error: any })
3751

52+
/**
53+
* Internal version of `ActionCreatorWithPreparedPayload`. Not to be used externally.
54+
*
55+
* @internal
56+
*/
3857
export type _ActionCreatorWithPreparedPayload<
3958
PA extends PrepareAction<any> | void,
4059
T extends string = string
@@ -56,46 +75,129 @@ export type _ActionCreatorWithPreparedPayload<
5675
>
5776
: void
5877

78+
/**
79+
* Basic type for all action creators.
80+
*
81+
* @inheritdoc {redux#ActionCreator}
82+
*/
5983
interface BaseActionCreator<P, T extends string, M = never, E = never> {
6084
type: T
6185
match(action: Action<unknown>): action is PayloadAction<P, T, M, E>
6286
}
6387

88+
/**
89+
* An action creator that takes multiple arguments that are passed
90+
* to a `PrepareAction` method to create the final Action.
91+
* @typeParam Args arguments for the action creator function
92+
* @typeParam P `payload` type
93+
* @typeParam T `type` name
94+
* @typeParam E optional `error` type
95+
* @typeParam M optional `meta` type
96+
*
97+
* @inheritdoc {redux#ActionCreator}
98+
*
99+
* @public
100+
*/
64101
export interface ActionCreatorWithPreparedPayload<
65102
Args extends unknown[],
66103
P,
67104
T extends string = string,
68105
E = never,
69106
M = never
70107
> extends BaseActionCreator<P, T, M, E> {
108+
/**
109+
* Calling this {@link redux#ActionCreator} with `Args` will return
110+
* an Action with a payload of type `P` and (depending on the `PrepareAction`
111+
* method used) a `meta`- and `error` property of types `M` and `E` respectively.
112+
*/
71113
(...args: Args): PayloadAction<P, T, M, E>
72114
}
73115

116+
/**
117+
* An action creator of type `T` that takes an optional payload of type `P`.
118+
*
119+
* @inheritdoc {redux#ActionCreator}
120+
*
121+
* @public
122+
*/
74123
export interface ActionCreatorWithOptionalPayload<P, T extends string = string>
75124
extends BaseActionCreator<P, T> {
125+
/**
126+
* Calling this {@link redux#ActionCreator} without arguments will
127+
* return a {@link PayloadAction} of type `T` with a payload of `undefined`
128+
*/
76129
(payload?: undefined): PayloadAction<undefined, T>
130+
/**
131+
* Calling this {@link redux#ActionCreator} with an argument will
132+
* return a {@link PayloadAction} of type `T` with a payload of `P`
133+
*/
77134
<PT extends Diff<P, undefined>>(payload?: PT): PayloadAction<PT, T>
78135
}
79136

137+
/**
138+
* An action creator of type `T` that takes no payload.
139+
*
140+
* @inheritdoc {redux#ActionCreator}
141+
*
142+
* @public
143+
*/
80144
export interface ActionCreatorWithoutPayload<T extends string = string>
81145
extends BaseActionCreator<undefined, T> {
146+
/**
147+
* Calling this {@link redux#ActionCreator} will
148+
* return a {@link PayloadAction} of type `T` with a payload of `undefined`
149+
*/
82150
(): PayloadAction<undefined, T>
83151
}
84152

153+
/**
154+
* An action creator of type `T` that requires a payload of type P.
155+
*
156+
* @inheritdoc {redux#ActionCreator}
157+
*
158+
* @public
159+
*/
85160
export interface ActionCreatorWithPayload<P, T extends string = string>
86161
extends BaseActionCreator<P, T> {
162+
/**
163+
* Calling this {@link redux#ActionCreator} with an argument will
164+
* return a {@link PayloadAction} of type `T` with a payload of `P`
165+
* If possible, `P` will be narrowed down to the exact type of the payload argument.
166+
*/
87167
<PT extends P>(payload: PT): PayloadAction<PT, T>
168+
/**
169+
* Calling this {@link redux#ActionCreator} with an argument will
170+
* return a {@link PayloadAction} of type `T` with a payload of `P`
171+
*/
88172
(payload: P): PayloadAction<P, T>
89173
}
90174

175+
/**
176+
* An action creator of type `T` whose `payload` type could not be inferred. Accepts everything as `payload`.
177+
*
178+
* @inheritdoc {redux#ActionCreator}
179+
*
180+
* @public
181+
*/
91182
export interface ActionCreatorWithNonInferrablePayload<
92183
T extends string = string
93184
> extends BaseActionCreator<unknown, T> {
185+
/**
186+
* Calling this {@link redux#ActionCreator} with an argument will
187+
* return a {@link PayloadAction} of type `T` with a payload
188+
* of exactly the type of the argument.
189+
*/
94190
<PT extends unknown>(payload: PT): PayloadAction<PT, T>
95191
}
96192

97193
/**
98194
* An action creator that produces actions with a `payload` attribute.
195+
*
196+
* @typeParam P the `payload` type
197+
* @typeParam T the `type` of the resulting action
198+
* @typeParam PA if the resulting action is preprocessed by a `prepare` method, the signature of said method.
199+
*
200+
* @public
99201
*/
100202
export type PayloadActionCreator<
101203
P = void,
@@ -105,19 +207,23 @@ export type PayloadActionCreator<
105207
PA,
106208
_ActionCreatorWithPreparedPayload<PA, T>,
107209
// else
108-
IsUnknownOrNonInferrable<
210+
IsAny<
109211
P,
110-
ActionCreatorWithNonInferrablePayload<T>,
111-
// else
112-
IfVoid<
212+
ActionCreatorWithPayload<any, T>,
213+
IsUnknownOrNonInferrable<
113214
P,
114-
ActionCreatorWithoutPayload<T>,
215+
ActionCreatorWithNonInferrablePayload<T>,
115216
// else
116-
IfMaybeUndefined<
217+
IfVoid<
117218
P,
118-
ActionCreatorWithOptionalPayload<P, T>,
219+
ActionCreatorWithoutPayload<T>,
119220
// else
120-
ActionCreatorWithPayload<P, T>
221+
IfMaybeUndefined<
222+
P,
223+
ActionCreatorWithOptionalPayload<P, T>,
224+
// else
225+
ActionCreatorWithPayload<P, T>
226+
>
121227
>
122228
>
123229
>
@@ -133,12 +239,26 @@ export type PayloadActionCreator<
133239
* @param type The action type to use for created actions.
134240
* @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
135241
* If this is given, the resulting action creator will pass it's arguments to this method to calculate payload & meta.
242+
*
243+
* @public
136244
*/
137-
138245
export function createAction<P = void, T extends string = string>(
139246
type: T
140247
): PayloadActionCreator<P, T>
141248

249+
/**
250+
* A utility function to create an action creator for the given action type
251+
* string. The action creator accepts a single argument, which will be included
252+
* in the action object as a field called payload. The action creator function
253+
* will also have its toString() overriden so that it returns the action type,
254+
* allowing it to be used in reducer logic that is looking for that action type.
255+
*
256+
* @param type The action type to use for created actions.
257+
* @param prepare (optional) a method that takes any number of arguments and returns { payload } or { payload, meta }.
258+
* If this is given, the resulting action creator will pass it's arguments to this method to calculate payload & meta.
259+
*
260+
* @public
261+
*/
142262
export function createAction<
143263
PA extends PrepareAction<any>,
144264
T extends string = string
@@ -182,6 +302,8 @@ export function createAction(type: string, prepareAction?: Function): any {
182302
*
183303
* @param action The action creator whose action type to get.
184304
* @returns The action type used by the action creator.
305+
*
306+
* @public
185307
*/
186308
export function getType<T extends string>(
187309
actionCreator: PayloadActionCreator<any, T>

0 commit comments

Comments
 (0)