UNPKG

13.2 kBPlain TextView Raw
1import type { Action, AnyAction, Reducer } from 'redux'
2import type {
3 ActionCreatorWithNonInferrablePayload,
4 ActionCreatorWithOptionalPayload,
5 ActionCreatorWithoutPayload,
6 ActionCreatorWithPayload,
7 ActionCreatorWithPreparedPayload,
8 ActionReducerMapBuilder,
9 PayloadAction,
10 SliceCaseReducers,
11 ValidateSliceCaseReducers,
12} from '@reduxjs/toolkit'
13import { createAction, createSlice } from '@reduxjs/toolkit'
14import { expectType } from './helpers'
15
16/*
17 * Test: Slice name is strongly typed.
18 */
19
20const counterSlice = createSlice({
21 name: 'counter',
22 initialState: 0,
23 reducers: {
24 increment: (state: number, action) => state + action.payload,
25 decrement: (state: number, action) => state - action.payload,
26 },
27})
28
29const uiSlice = createSlice({
30 name: 'ui',
31 initialState: 0,
32 reducers: {
33 goToNext: (state: number, action) => state + action.payload,
34 goToPrevious: (state: number, action) => state - action.payload,
35 },
36})
37
38const actionCreators = {
39 [counterSlice.name]: { ...counterSlice.actions },
40 [uiSlice.name]: { ...uiSlice.actions },
41}
42
43expectType<typeof counterSlice.actions>(actionCreators.counter)
44expectType<typeof uiSlice.actions>(actionCreators.ui)
45
46// @ts-expect-error
47const value = actionCreators.anyKey
48
49/*
50 * Test: createSlice() infers the returned slice's type.
51 */
52{
53 const firstAction = createAction<{ count: number }>('FIRST_ACTION')
54
55 const slice = createSlice({
56 name: 'counter',
57 initialState: 0,
58 reducers: {
59 increment: (state: number, action) => state + action.payload,
60 decrement: (state: number, action) => state - action.payload,
61 },
62 extraReducers: {
63 [firstAction.type]: (state: number, action) =>
64 state + action.payload.count,
65 },
66 })
67
68 /* Reducer */
69
70 const reducer: Reducer<number, PayloadAction> = slice.reducer
71
72 // @ts-expect-error
73 const stringReducer: Reducer<string, PayloadAction> = slice.reducer
74 // @ts-expect-error
75 const anyActionReducer: Reducer<string, AnyAction> = slice.reducer
76
77 /* Actions */
78
79 slice.actions.increment(1)
80 slice.actions.decrement(1)
81
82 // @ts-expect-error
83 slice.actions.other(1)
84}
85
86/*
87 * Test: Slice action creator types are inferred.
88 */
89{
90 const counter = createSlice({
91 name: 'counter',
92 initialState: 0,
93 reducers: {
94 increment: (state) => state + 1,
95 decrement: (state, { payload = 1 }: PayloadAction<number | undefined>) =>
96 state - payload,
97 multiply: (state, { payload }: PayloadAction<number | number[]>) =>
98 Array.isArray(payload)
99 ? payload.reduce((acc, val) => acc * val, state)
100 : state * payload,
101 addTwo: {
102 reducer: (s, { payload }: PayloadAction<number>) => s + payload,
103 prepare: (a: number, b: number) => ({
104 payload: a + b,
105 }),
106 },
107 },
108 })
109
110 expectType<ActionCreatorWithoutPayload>(counter.actions.increment)
111 counter.actions.increment()
112
113 expectType<ActionCreatorWithOptionalPayload<number | undefined>>(
114 counter.actions.decrement
115 )
116 counter.actions.decrement()
117 counter.actions.decrement(2)
118
119 expectType<ActionCreatorWithPayload<number | number[]>>(
120 counter.actions.multiply
121 )
122 counter.actions.multiply(2)
123 counter.actions.multiply([2, 3, 4])
124
125 expectType<ActionCreatorWithPreparedPayload<[number, number], number>>(
126 counter.actions.addTwo
127 )
128 counter.actions.addTwo(1, 2)
129
130 // @ts-expect-error
131 counter.actions.multiply()
132
133 // @ts-expect-error
134 counter.actions.multiply('2')
135
136 // @ts-expect-error
137 counter.actions.addTwo(1)
138}
139
140/*
141 * Test: Slice action creator types properties are "string"
142 */
143{
144 const counter = createSlice({
145 name: 'counter',
146 initialState: 0,
147 reducers: {
148 increment: (state) => state + 1,
149 decrement: (state) => state - 1,
150 multiply: (state, { payload }: PayloadAction<number | number[]>) =>
151 Array.isArray(payload)
152 ? payload.reduce((acc, val) => acc * val, state)
153 : state * payload,
154 },
155 })
156
157 const s: string = counter.actions.increment.type
158 const t: string = counter.actions.decrement.type
159 const u: string = counter.actions.multiply.type
160
161 // @ts-expect-error
162 const x: 'counter/increment' = counter.actions.increment.type
163 // @ts-expect-error
164 const y: 'increment' = counter.actions.increment.type
165}
166
167/*
168 * Test: Slice action creator types are inferred for enhanced reducers.
169 */
170{
171 const counter = createSlice({
172 name: 'test',
173 initialState: { counter: 0, concat: '' },
174 reducers: {
175 incrementByStrLen: {
176 reducer: (state, action: PayloadAction<number>) => {
177 state.counter += action.payload
178 },
179 prepare: (payload: string) => ({
180 payload: payload.length,
181 }),
182 },
183 concatMetaStrLen: {
184 reducer: (state, action: PayloadAction<string>) => {
185 state.concat += action.payload
186 },
187 prepare: (payload: string) => ({
188 payload,
189 meta: payload.length,
190 }),
191 },
192 },
193 })
194
195 expectType<string>(counter.actions.incrementByStrLen('test').type)
196 expectType<number>(counter.actions.incrementByStrLen('test').payload)
197 expectType<string>(counter.actions.concatMetaStrLen('test').payload)
198 expectType<number>(counter.actions.concatMetaStrLen('test').meta)
199
200 // @ts-expect-error
201 expectType<string>(counter.actions.incrementByStrLen('test').payload)
202
203 // @ts-expect-error
204 expectType<string>(counter.actions.concatMetaStrLen('test').meta)
205}
206
207/**
208 * Test: access meta and error from reducer
209 */
210{
211 const counter = createSlice({
212 name: 'test',
213 initialState: { counter: 0, concat: '' },
214 reducers: {
215 // case: meta and error not used in reducer
216 testDefaultMetaAndError: {
217 reducer(_, action: PayloadAction<number, string>) {},
218 prepare: (payload: number) => ({
219 payload,
220 meta: 'meta' as 'meta',
221 error: 'error' as 'error',
222 }),
223 },
224 // case: meta and error marked as "unknown" in reducer
225 testUnknownMetaAndError: {
226 reducer(_, action: PayloadAction<number, string, unknown, unknown>) {},
227 prepare: (payload: number) => ({
228 payload,
229 meta: 'meta' as 'meta',
230 error: 'error' as 'error',
231 }),
232 },
233 // case: meta and error are typed in the reducer as returned by prepare
234 testMetaAndError: {
235 reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
236 prepare: (payload: number) => ({
237 payload,
238 meta: 'meta' as 'meta',
239 error: 'error' as 'error',
240 }),
241 },
242 // case: meta is typed differently in the reducer than returned from prepare
243 testErroneousMeta: {
244 reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
245 // @ts-expect-error
246 prepare: (payload: number) => ({
247 payload,
248 meta: 1,
249 error: 'error' as 'error',
250 }),
251 },
252 // case: error is typed differently in the reducer than returned from prepare
253 testErroneousError: {
254 reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
255 // @ts-expect-error
256 prepare: (payload: number) => ({
257 payload,
258 meta: 'meta' as 'meta',
259 error: 1,
260 }),
261 },
262 },
263 })
264}
265
266/*
267 * Test: returned case reducer has the correct type
268 */
269{
270 const counter = createSlice({
271 name: 'counter',
272 initialState: 0,
273 reducers: {
274 increment(state, action: PayloadAction<number>) {
275 return state + action.payload
276 },
277 decrement: {
278 reducer(state, action: PayloadAction<number>) {
279 return state - action.payload
280 },
281 prepare(amount: number) {
282 return { payload: amount }
283 },
284 },
285 },
286 })
287
288 // Should match positively
289 expectType<(state: number, action: PayloadAction<number>) => number | void>(
290 counter.caseReducers.increment
291 )
292
293 // Should match positively for reducers with prepare callback
294 expectType<(state: number, action: PayloadAction<number>) => number | void>(
295 counter.caseReducers.decrement
296 )
297
298 // Should not mismatch the payload if it's a simple reducer
299
300 expectType<(state: number, action: PayloadAction<string>) => number | void>(
301 // @ts-expect-error
302 counter.caseReducers.increment
303 )
304
305 // Should not mismatch the payload if it's a reducer with a prepare callback
306
307 expectType<(state: number, action: PayloadAction<string>) => number | void>(
308 // @ts-expect-error
309 counter.caseReducers.decrement
310 )
311
312 // Should not include entries that don't exist
313
314 expectType<(state: number, action: PayloadAction<string>) => number | void>(
315 // @ts-expect-error
316 counter.caseReducers.someThingNonExistant
317 )
318}
319
320/*
321 * Test: prepared payload does not match action payload - should cause an error.
322 */
323{
324 const counter = createSlice({
325 name: 'counter',
326 initialState: { counter: 0 },
327 reducers: {
328 increment: {
329 reducer(state, action: PayloadAction<string>) {
330 state.counter += action.payload.length
331 },
332 // @ts-expect-error
333 prepare(x: string) {
334 return {
335 payload: 6,
336 }
337 },
338 },
339 },
340 })
341}
342
343/*
344 * Test: if no Payload Type is specified, accept any payload
345 * see https://github.com/reduxjs/redux-toolkit/issues/165
346 */
347{
348 const initialState = {
349 name: null,
350 }
351
352 const mySlice = createSlice({
353 name: 'name',
354 initialState,
355 reducers: {
356 setName: (state, action) => {
357 state.name = action.payload
358 },
359 },
360 })
361
362 expectType<ActionCreatorWithNonInferrablePayload>(mySlice.actions.setName)
363
364 const x = mySlice.actions.setName
365
366 mySlice.actions.setName(null)
367 mySlice.actions.setName('asd')
368 mySlice.actions.setName(5)
369}
370
371/**
372 * Test: actions.x.match()
373 */
374{
375 const mySlice = createSlice({
376 name: 'name',
377 initialState: { name: 'test' },
378 reducers: {
379 setName: (state, action: PayloadAction<string>) => {
380 state.name = action.payload
381 },
382 },
383 })
384
385 const x: Action<unknown> = {} as any
386 if (mySlice.actions.setName.match(x)) {
387 expectType<string>(x.type)
388 expectType<string>(x.payload)
389 } else {
390 // @ts-expect-error
391 expectType<string>(x.type)
392 // @ts-expect-error
393 expectType<string>(x.payload)
394 }
395}
396
397/** Test: alternative builder callback for extraReducers */
398{
399 createSlice({
400 name: 'test',
401 initialState: 0,
402 reducers: {},
403 extraReducers: (builder) => {
404 expectType<ActionReducerMapBuilder<number>>(builder)
405 },
406 })
407}
408
409/** Test: wrapping createSlice should be possible */
410{
411 interface GenericState<T> {
412 data?: T
413 status: 'loading' | 'finished' | 'error'
414 }
415
416 const createGenericSlice = <
417 T,
418 Reducers extends SliceCaseReducers<GenericState<T>>
419 >({
420 name = '',
421 initialState,
422 reducers,
423 }: {
424 name: string
425 initialState: GenericState<T>
426 reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>
427 }) => {
428 return createSlice({
429 name,
430 initialState,
431 reducers: {
432 start(state) {
433 state.status = 'loading'
434 },
435 success(state: GenericState<T>, action: PayloadAction<T>) {
436 state.data = action.payload
437 state.status = 'finished'
438 },
439 ...reducers,
440 },
441 })
442 }
443
444 const wrappedSlice = createGenericSlice({
445 name: 'test',
446 initialState: { status: 'loading' } as GenericState<string>,
447 reducers: {
448 magic(state) {
449 expectType<GenericState<string>>(state)
450 // @ts-expect-error
451 expectType<GenericState<number>>(state)
452
453 state.status = 'finished'
454 state.data = 'hocus pocus'
455 },
456 },
457 })
458
459 expectType<ActionCreatorWithPayload<string>>(wrappedSlice.actions.success)
460 expectType<ActionCreatorWithoutPayload<string>>(wrappedSlice.actions.magic)
461}
462
463{
464 interface GenericState<T> {
465 data: T | null
466 }
467
468 function createDataSlice<
469 T,
470 Reducers extends SliceCaseReducers<GenericState<T>>
471 >(
472 name: string,
473 reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>,
474 initialState: GenericState<T>
475 ) {
476 const doNothing = createAction<undefined>('doNothing')
477 const setData = createAction<T>('setData')
478
479 const slice = createSlice({
480 name,
481 initialState,
482 reducers,
483 extraReducers: (builder) => {
484 builder.addCase(doNothing, (state) => {
485 return { ...state }
486 })
487 builder.addCase(setData, (state, { payload }) => {
488 return {
489 ...state,
490 data: payload,
491 }
492 })
493 },
494 })
495 return { doNothing, setData, slice }
496 }
497}