| [a762898] | 1 | import type {
|
|---|
| 2 | Action,
|
|---|
| 3 | Reducer,
|
|---|
| 4 | Slice,
|
|---|
| 5 | WithSlice,
|
|---|
| 6 | WithSlicePreloadedState,
|
|---|
| 7 | } from '@reduxjs/toolkit'
|
|---|
| 8 | import { combineSlices } from '@reduxjs/toolkit'
|
|---|
| 9 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
|
|---|
| 10 |
|
|---|
| 11 | declare const stringSlice: Slice<string, {}, 'string'>
|
|---|
| 12 |
|
|---|
| 13 | declare const numberSlice: Slice<number, {}, 'number'>
|
|---|
| 14 |
|
|---|
| 15 | declare const booleanReducer: Reducer<boolean>
|
|---|
| 16 |
|
|---|
| 17 | declare const mixedReducer: Reducer<string, Action, number>
|
|---|
| 18 |
|
|---|
| 19 | declare const mixedSliceLike: {
|
|---|
| 20 | reducerPath: 'mixedSlice'
|
|---|
| 21 | reducer: typeof mixedReducer
|
|---|
| 22 | }
|
|---|
| 23 |
|
|---|
| 24 | const exampleApi = createApi({
|
|---|
| 25 | baseQuery: fetchBaseQuery(),
|
|---|
| 26 | endpoints: (build) => ({
|
|---|
| 27 | getThing: build.query({
|
|---|
| 28 | query: () => '',
|
|---|
| 29 | }),
|
|---|
| 30 | }),
|
|---|
| 31 | })
|
|---|
| 32 |
|
|---|
| 33 | type ExampleApiState = ReturnType<typeof exampleApi.reducer>
|
|---|
| 34 |
|
|---|
| 35 | describe('type tests', () => {
|
|---|
| 36 | test('combineSlices correctly combines static state', () => {
|
|---|
| 37 | const rootReducer = combineSlices(
|
|---|
| 38 | stringSlice,
|
|---|
| 39 | numberSlice,
|
|---|
| 40 | exampleApi,
|
|---|
| 41 | {
|
|---|
| 42 | boolean: booleanReducer,
|
|---|
| 43 | mixed: mixedReducer,
|
|---|
| 44 | },
|
|---|
| 45 | mixedSliceLike,
|
|---|
| 46 | )
|
|---|
| 47 |
|
|---|
| 48 | expectTypeOf(rootReducer(undefined, { type: '' })).toEqualTypeOf<{
|
|---|
| 49 | string: string
|
|---|
| 50 | number: number
|
|---|
| 51 | boolean: boolean
|
|---|
| 52 | api: ExampleApiState
|
|---|
| 53 | mixed: string
|
|---|
| 54 | mixedSlice: string
|
|---|
| 55 | }>()
|
|---|
| 56 |
|
|---|
| 57 | // test for correct preloaded state handling
|
|---|
| 58 | expectTypeOf(rootReducer).toBeCallableWith(
|
|---|
| 59 | { mixed: 9, mixedSlice: 9 },
|
|---|
| 60 | { type: '' },
|
|---|
| 61 | )
|
|---|
| 62 | })
|
|---|
| 63 |
|
|---|
| 64 | test('combineSlices allows passing no initial reducers', () => {
|
|---|
| 65 | const rootReducer = combineSlices()
|
|---|
| 66 |
|
|---|
| 67 | expectTypeOf(rootReducer(undefined, { type: '' })).toEqualTypeOf<{}>()
|
|---|
| 68 |
|
|---|
| 69 | const declaredLazy =
|
|---|
| 70 | combineSlices().withLazyLoadedSlices<WithSlice<typeof numberSlice>>()
|
|---|
| 71 |
|
|---|
| 72 | expectTypeOf(declaredLazy(undefined, { type: '' })).toEqualTypeOf<{
|
|---|
| 73 | number?: number
|
|---|
| 74 | }>()
|
|---|
| 75 | })
|
|---|
| 76 |
|
|---|
| 77 | test('withLazyLoadedSlices adds partial to state', () => {
|
|---|
| 78 | const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
|
|---|
| 79 | WithSlice<typeof numberSlice> & WithSlice<typeof exampleApi>
|
|---|
| 80 | >()
|
|---|
| 81 |
|
|---|
| 82 | expectTypeOf(rootReducer(undefined, { type: '' }).number).toEqualTypeOf<
|
|---|
| 83 | number | undefined
|
|---|
| 84 | >()
|
|---|
| 85 |
|
|---|
| 86 | expectTypeOf(rootReducer(undefined, { type: '' }).api).toEqualTypeOf<
|
|---|
| 87 | ExampleApiState | undefined
|
|---|
| 88 | >()
|
|---|
| 89 | })
|
|---|
| 90 |
|
|---|
| 91 | test('inject marks injected keys as required', () => {
|
|---|
| 92 | const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
|
|---|
| 93 | WithSlice<typeof numberSlice> &
|
|---|
| 94 | WithSlice<typeof exampleApi> & { boolean: boolean } & WithSlice<
|
|---|
| 95 | typeof mixedSliceLike
|
|---|
| 96 | > &
|
|---|
| 97 | WithSlice<{
|
|---|
| 98 | reducerPath: 'mixedReducer'
|
|---|
| 99 | reducer: typeof mixedReducer
|
|---|
| 100 | }>,
|
|---|
| 101 | WithSlicePreloadedState<typeof numberSlice> &
|
|---|
| 102 | WithSlicePreloadedState<typeof exampleApi> & {
|
|---|
| 103 | boolean: boolean
|
|---|
| 104 | } & WithSlicePreloadedState<typeof mixedSliceLike> &
|
|---|
| 105 | WithSlicePreloadedState<{
|
|---|
| 106 | reducerPath: 'mixedReducer'
|
|---|
| 107 | reducer: typeof mixedReducer
|
|---|
| 108 | }>
|
|---|
| 109 | >()
|
|---|
| 110 |
|
|---|
| 111 | expectTypeOf(rootReducer(undefined, { type: '' }).number).toEqualTypeOf<
|
|---|
| 112 | number | undefined
|
|---|
| 113 | >()
|
|---|
| 114 |
|
|---|
| 115 | expectTypeOf(rootReducer(undefined, { type: '' }).boolean).toEqualTypeOf<
|
|---|
| 116 | boolean | undefined
|
|---|
| 117 | >()
|
|---|
| 118 |
|
|---|
| 119 | expectTypeOf(rootReducer(undefined, { type: '' }).api).toEqualTypeOf<
|
|---|
| 120 | ExampleApiState | undefined
|
|---|
| 121 | >()
|
|---|
| 122 |
|
|---|
| 123 | expectTypeOf(rootReducer(undefined, { type: '' }).mixedSlice).toEqualTypeOf<
|
|---|
| 124 | string | undefined
|
|---|
| 125 | >()
|
|---|
| 126 |
|
|---|
| 127 | expectTypeOf(
|
|---|
| 128 | rootReducer(undefined, { type: '' }).mixedReducer,
|
|---|
| 129 | ).toEqualTypeOf<string | undefined>()
|
|---|
| 130 |
|
|---|
| 131 | const withNumber = rootReducer.inject(numberSlice)
|
|---|
| 132 |
|
|---|
| 133 | expectTypeOf(withNumber(undefined, { type: '' }).number).toBeNumber()
|
|---|
| 134 |
|
|---|
| 135 | const withBool = rootReducer.inject({
|
|---|
| 136 | reducerPath: 'boolean' as const,
|
|---|
| 137 | reducer: booleanReducer,
|
|---|
| 138 | })
|
|---|
| 139 |
|
|---|
| 140 | expectTypeOf(withBool(undefined, { type: '' }).boolean).toBeBoolean()
|
|---|
| 141 |
|
|---|
| 142 | const withApi = rootReducer.inject(exampleApi)
|
|---|
| 143 |
|
|---|
| 144 | expectTypeOf(
|
|---|
| 145 | withApi(undefined, { type: '' }).api,
|
|---|
| 146 | ).toEqualTypeOf<ExampleApiState>()
|
|---|
| 147 |
|
|---|
| 148 | const withMixedSlice = rootReducer.inject(mixedSliceLike)
|
|---|
| 149 |
|
|---|
| 150 | expectTypeOf(
|
|---|
| 151 | withMixedSlice(undefined, { type: '' }).mixedSlice,
|
|---|
| 152 | ).toBeString()
|
|---|
| 153 |
|
|---|
| 154 | const withMixedReducer = rootReducer.inject({
|
|---|
| 155 | reducerPath: 'mixedReducer',
|
|---|
| 156 | reducer: mixedReducer,
|
|---|
| 157 | })
|
|---|
| 158 |
|
|---|
| 159 | expectTypeOf(
|
|---|
| 160 | withMixedReducer(undefined, { type: '' }).mixedReducer,
|
|---|
| 161 | ).toBeString()
|
|---|
| 162 | })
|
|---|
| 163 |
|
|---|
| 164 | test('selector() allows defining selectors with injected reducers defined', () => {
|
|---|
| 165 | const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
|
|---|
| 166 | WithSlice<typeof numberSlice> & { boolean: boolean }
|
|---|
| 167 | >()
|
|---|
| 168 |
|
|---|
| 169 | type RootState = ReturnType<typeof rootReducer>
|
|---|
| 170 |
|
|---|
| 171 | const withoutInjection = rootReducer.selector(
|
|---|
| 172 | (state: RootState) => state.number,
|
|---|
| 173 | )
|
|---|
| 174 |
|
|---|
| 175 | expectTypeOf(
|
|---|
| 176 | withoutInjection(rootReducer(undefined, { type: '' })),
|
|---|
| 177 | ).toEqualTypeOf<number | undefined>()
|
|---|
| 178 |
|
|---|
| 179 | const withInjection = rootReducer
|
|---|
| 180 | .inject(numberSlice)
|
|---|
| 181 | .selector((state) => state.number)
|
|---|
| 182 |
|
|---|
| 183 | expectTypeOf(
|
|---|
| 184 | withInjection(rootReducer(undefined, { type: '' })),
|
|---|
| 185 | ).toBeNumber()
|
|---|
| 186 | })
|
|---|
| 187 |
|
|---|
| 188 | test('selector() passes arguments through', () => {
|
|---|
| 189 | const rootReducer = combineSlices(stringSlice).withLazyLoadedSlices<
|
|---|
| 190 | WithSlice<typeof numberSlice> & { boolean: boolean }
|
|---|
| 191 | >()
|
|---|
| 192 |
|
|---|
| 193 | const selector = rootReducer
|
|---|
| 194 | .inject(numberSlice)
|
|---|
| 195 | .selector((state, num: number) => state.number)
|
|---|
| 196 |
|
|---|
| 197 | const state = rootReducer(undefined, { type: '' })
|
|---|
| 198 |
|
|---|
| 199 | expectTypeOf(selector).toBeCallableWith(state, 0)
|
|---|
| 200 |
|
|---|
| 201 | // required argument
|
|---|
| 202 | expectTypeOf(selector).parameters.not.toMatchTypeOf([state])
|
|---|
| 203 |
|
|---|
| 204 | // number not string
|
|---|
| 205 | expectTypeOf(selector).parameters.not.toMatchTypeOf([state, ''])
|
|---|
| 206 | })
|
|---|
| 207 |
|
|---|
| 208 | test('nested calls inferred correctly', () => {
|
|---|
| 209 | const innerReducer =
|
|---|
| 210 | combineSlices(stringSlice).withLazyLoadedSlices<
|
|---|
| 211 | WithSlice<typeof numberSlice>
|
|---|
| 212 | >()
|
|---|
| 213 |
|
|---|
| 214 | const innerSelector = innerReducer.inject(numberSlice).selector(
|
|---|
| 215 | (state) => state.number,
|
|---|
| 216 | (rootState: RootState) => rootState.inner,
|
|---|
| 217 | )
|
|---|
| 218 |
|
|---|
| 219 | const outerReducer = combineSlices({ inner: innerReducer })
|
|---|
| 220 |
|
|---|
| 221 | type RootState = ReturnType<typeof outerReducer>
|
|---|
| 222 |
|
|---|
| 223 | expectTypeOf(outerReducer(undefined, { type: '' })).toMatchTypeOf<{
|
|---|
| 224 | inner: { string: string }
|
|---|
| 225 | }>()
|
|---|
| 226 |
|
|---|
| 227 | expectTypeOf(
|
|---|
| 228 | innerSelector(outerReducer(undefined, { type: '' })),
|
|---|
| 229 | ).toBeNumber()
|
|---|
| 230 | })
|
|---|
| 231 |
|
|---|
| 232 | test('selector errors if selectorFn and selectState are mismatched', () => {
|
|---|
| 233 | const combinedReducer =
|
|---|
| 234 | combineSlices(stringSlice).withLazyLoadedSlices<
|
|---|
| 235 | WithSlice<typeof numberSlice>
|
|---|
| 236 | >()
|
|---|
| 237 |
|
|---|
| 238 | const outerReducer = combineSlices({ inner: combinedReducer })
|
|---|
| 239 |
|
|---|
| 240 | type RootState = ReturnType<typeof outerReducer>
|
|---|
| 241 |
|
|---|
| 242 | combinedReducer.selector(
|
|---|
| 243 | (state) => state.number,
|
|---|
| 244 | // @ts-expect-error wrong state returned
|
|---|
| 245 | (rootState: RootState) => rootState.inner.number,
|
|---|
| 246 | )
|
|---|
| 247 |
|
|---|
| 248 | combinedReducer.selector(
|
|---|
| 249 | (state, num: number) => state.number,
|
|---|
| 250 | // @ts-expect-error wrong arguments
|
|---|
| 251 | (rootState: RootState, str: string) => rootState.inner,
|
|---|
| 252 | )
|
|---|
| 253 |
|
|---|
| 254 | combinedReducer.selector(
|
|---|
| 255 | (state, num: number) => state.number,
|
|---|
| 256 | (rootState: RootState) => rootState.inner,
|
|---|
| 257 | )
|
|---|
| 258 |
|
|---|
| 259 | // TODO: see if there's a way of making this work
|
|---|
| 260 | // probably a rare case so not the end of the world if not
|
|---|
| 261 | combinedReducer.selector(
|
|---|
| 262 | (state) => state.number,
|
|---|
| 263 | // @ts-ignore
|
|---|
| 264 | (rootState: RootState, num: number) => rootState.inner,
|
|---|
| 265 | )
|
|---|
| 266 | })
|
|---|
| 267 |
|
|---|
| 268 | test('correct type of state is inferred when not declared via `withLazyLoadedSlices`', () => {
|
|---|
| 269 | // Related to https://github.com/reduxjs/redux-toolkit/issues/4171
|
|---|
| 270 |
|
|---|
| 271 | const combinedReducer = combineSlices(stringSlice)
|
|---|
| 272 |
|
|---|
| 273 | const withNumber = combinedReducer.inject(numberSlice)
|
|---|
| 274 |
|
|---|
| 275 | expectTypeOf(withNumber).returns.toEqualTypeOf<{
|
|---|
| 276 | string: string
|
|---|
| 277 | number: number
|
|---|
| 278 | }>()
|
|---|
| 279 |
|
|---|
| 280 | expectTypeOf(
|
|---|
| 281 | withNumber(undefined, { type: '' }).number,
|
|---|
| 282 | ).toMatchTypeOf<number>()
|
|---|
| 283 | })
|
|---|
| 284 | })
|
|---|