Skip to content

Commit 7e02ab4

Browse files
authored
correctly enforce meta and error types as returned from prepare (#350)
* correctly enforce meta and error types as returned from `prepare` * api report * alternative approach for TS<3.5
1 parent 1e993ee commit 7e02ab4

File tree

4 files changed

+71
-13
lines changed

4 files changed

+71
-13
lines changed

etc/redux-toolkit.api.md

+4-8
Original file line numberDiff line numberDiff line change
@@ -194,21 +194,17 @@ export type SliceActionCreator<P> = PayloadActionCreator<P>;
194194

195195
// @public
196196
export type SliceCaseReducers<State> = {
197-
[K: string]: CaseReducer<State, PayloadAction<any>> | CaseReducerWithPrepare<State, PayloadAction<any>>;
197+
[K: string]: CaseReducer<State, PayloadAction<any>> | CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>;
198198
};
199199

200200
export { ThunkAction }
201201

202202
// @public
203203
export type ValidateSliceCaseReducers<S, ACR extends SliceCaseReducers<S>> = ACR & {
204-
[P in keyof ACR]: ACR[P] extends {
205-
reducer(s: S, action?: {
206-
payload: infer O;
207-
}): any;
204+
[T in keyof ACR]: ACR[T] extends {
205+
reducer(s: S, action?: infer A): any;
208206
} ? {
209-
prepare(...a: never[]): {
210-
payload: O;
211-
};
207+
prepare(...a: never[]): Omit<A, 'type'>;
212208
} : {};
213209
};
214210

src/createSlice.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
ActionReducerMapBuilder,
1313
executeReducerBuilderCallback
1414
} from './mapBuilders'
15+
import { Omit } from './tsHelpers'
1516

1617
/**
1718
* An action creator atttached to a slice.
@@ -110,7 +111,7 @@ export type CaseReducerWithPrepare<State, Action extends PayloadAction> = {
110111
export type SliceCaseReducers<State> = {
111112
[K: string]:
112113
| CaseReducer<State, PayloadAction<any>>
113-
| CaseReducerWithPrepare<State, PayloadAction<any>>
114+
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
114115
}
115116

116117
/**
@@ -187,11 +188,11 @@ export type ValidateSliceCaseReducers<
187188
ACR extends SliceCaseReducers<S>
188189
> = ACR &
189190
{
190-
[P in keyof ACR]: ACR[P] extends {
191-
reducer(s: S, action?: { payload: infer O }): any
191+
[T in keyof ACR]: ACR[T] extends {
192+
reducer(s: S, action?: infer A): any
192193
}
193194
? {
194-
prepare(...a: never[]): { payload: O }
195+
prepare(...a: never[]): Omit<A, 'type'>
195196
}
196197
: {}
197198
}

src/tsHelpers.ts

+2
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,5 @@ type UnionToIntersection<U> = (U extends any
8787
: never) extends ((k: infer I) => void)
8888
? I
8989
: never
90+
91+
export type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>

type-tests/files/createSlice.typetest.ts

+60-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ function expectType<T>(t: T) {
7070
? payload.reduce((acc, val) => acc * val, state)
7171
: state * payload,
7272
addTwo: {
73-
reducer: (s, { payload }) => s + payload,
73+
reducer: (s, { payload }: PayloadAction<number>) => s + payload,
7474
prepare: (a: number, b: number) => ({
7575
payload: a + b
7676
})
@@ -175,6 +175,65 @@ function expectType<T>(t: T) {
175175
expectType<string>(counter.actions.concatMetaStrLen('test').meta)
176176
}
177177

178+
/**
179+
* Test: access meta and error from reducer
180+
*/
181+
{
182+
const counter = createSlice({
183+
name: 'test',
184+
initialState: { counter: 0, concat: '' },
185+
reducers: {
186+
// case: meta and error not used in reducer
187+
testDefaultMetaAndError: {
188+
reducer(_, action: PayloadAction<number, string>) {},
189+
prepare: (payload: number) => ({
190+
payload,
191+
meta: 'meta' as 'meta',
192+
error: 'error' as 'error'
193+
})
194+
},
195+
// case: meta and error marked as "unknown" in reducer
196+
testUnknownMetaAndError: {
197+
reducer(_, action: PayloadAction<number, string, unknown, unknown>) {},
198+
prepare: (payload: number) => ({
199+
payload,
200+
meta: 'meta' as 'meta',
201+
error: 'error' as 'error'
202+
})
203+
},
204+
// case: meta and error are typed in the reducer as returned by prepare
205+
testMetaAndError: {
206+
reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
207+
prepare: (payload: number) => ({
208+
payload,
209+
meta: 'meta' as 'meta',
210+
error: 'error' as 'error'
211+
})
212+
},
213+
// case: meta is typed differently in the reducer than returned from prepare
214+
testErroneousMeta: {
215+
reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
216+
// typings:expect-error
217+
prepare: (payload: number) => ({
218+
payload,
219+
meta: 1,
220+
error: 'error' as 'error'
221+
})
222+
},
223+
// case: error is typed differently in the reducer than returned from prepare
224+
testErroneousError: {
225+
reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
226+
// typings:expect-error
227+
prepare: (payload: number) => ({
228+
payload,
229+
meta: 'meta' as 'meta',
230+
error: 1
231+
})
232+
}
233+
}
234+
})
235+
}
236+
178237
/*
179238
* Test: returned case reducer has the correct type
180239
*/

0 commit comments

Comments
 (0)