diff --git a/docs/api/createSlice.mdx b/docs/api/createSlice.mdx index 2d0eab366a..826e0c61a2 100644 --- a/docs/api/createSlice.mdx +++ b/docs/api/createSlice.mdx @@ -1116,7 +1116,8 @@ const reducerCreatorType = Symbol() declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [reducerCreatorType]: ReducerCreatorEntry< @@ -1144,7 +1145,8 @@ const batchedCreatorType = Symbol() declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [batchedCreatorType]: ReducerCreatorEntry< @@ -1182,7 +1184,8 @@ const loaderCreatorType = Symbol() declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [loaderCreatorType]: ReducerCreatorEntry< @@ -1208,7 +1211,8 @@ const loaderCreatorType = Symbol() declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [loaderCreatorType]: ReducerCreatorEntry< @@ -1243,7 +1247,8 @@ const asyncThunkCreatorType = Symbol() declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [asyncThunkCreatorType]: ReducerCreatorEntry< @@ -1283,7 +1288,8 @@ const preparedReducerType = Symbol() declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [preparedReducerType]: ReducerCreatorEntry< @@ -1387,7 +1393,8 @@ interface ToastThunkCreator< declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [toastCreatorType]: ReducerCreatorEntry< @@ -1516,7 +1523,8 @@ interface PaginationState { declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [paginationCreatorType]: ReducerCreatorEntry< @@ -1586,7 +1594,7 @@ interface HistoryState { declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends CreatorCaseReducers = CreatorCaseReducers, Name extends string = string > { [paginationCreatorType]: ReducerCreatorEntry< @@ -1695,7 +1703,8 @@ interface UndoableMeta { declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [undoableCreatorType]: ReducerCreatorEntry< diff --git a/packages/toolkit/src/createSlice.ts b/packages/toolkit/src/createSlice.ts index a5f33966ef..3eebf7e529 100644 --- a/packages/toolkit/src/createSlice.ts +++ b/packages/toolkit/src/createSlice.ts @@ -63,9 +63,13 @@ export type ReducerCreatorEntry< : {} } +export type CreatorCaseReducers = + | Record + | SliceCaseReducers + export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [ReducerType.reducer]: ReducerCreatorEntry< @@ -315,7 +319,7 @@ export type ReducerCreator = { }) export type ReducerNamesOfType< - CaseReducers extends SliceCaseReducers, + CaseReducers extends CreatorCaseReducers, Type extends RegisteredReducerType, > = { [ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends ReducerDefinition @@ -334,7 +338,9 @@ interface InjectIntoConfig extends InjectConfig { */ export interface Slice< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + | SliceCaseReducers + | Record = SliceCaseReducers, Name extends string = string, ReducerPath extends string = Name, Selectors extends SliceSelectors = SliceSelectors, @@ -422,7 +428,9 @@ export interface Slice< */ interface InjectedSlice< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + | SliceCaseReducers + | Record = SliceCaseReducers, Name extends string = string, ReducerPath extends string = Name, Selectors extends SliceSelectors = SliceSelectors, @@ -463,6 +471,19 @@ interface InjectedSlice< selectSlice(state: { [K in ReducerPath]?: State | undefined }): State } +type CreatorCallback< + State, + CreatorMap extends Record, +> = ( + create: ReducerCreators, +) => Record + +type GetCaseReducers< + State, + CreatorMap extends Record, + CR extends SliceCaseReducers | CreatorCallback, +> = CR extends CreatorCallback ? ReturnType : CR + /** * Options for `createSlice()`. * @@ -470,7 +491,9 @@ interface InjectedSlice< */ export interface CreateSliceOptions< State = any, - CR extends SliceCaseReducers = SliceCaseReducers, + CR extends + | SliceCaseReducers + | CreatorCallback = SliceCaseReducers, Name extends string = string, ReducerPath extends string = Name, Selectors extends SliceSelectors = SliceSelectors, @@ -496,9 +519,7 @@ export interface CreateSliceOptions< * functions. For every action type, a matching action creator will be * generated using `createAction()`. */ - reducers: - | ValidateSliceCaseReducers - | ((create: ReducerCreators) => CR) + reducers: ValidateSliceCaseReducers /** * A callback that receives a *builder* object to define @@ -687,13 +708,11 @@ interface AsyncThunkCreator< * * @public */ -export type SliceCaseReducers = - | Record - | Record< - string, - | CaseReducer> - | CaseReducerWithPrepare> - > +export type SliceCaseReducers = Record< + string, + | CaseReducer> + | CaseReducerWithPrepare> +> /** * The type describing a slice's `selectors` option. @@ -713,7 +732,9 @@ export type SliceActionType< * @public */ export type CaseReducerActions< - CaseReducers extends SliceCaseReducers, + CaseReducers extends + | SliceCaseReducers + | Record, SliceName extends string, > = Id< UnionToIntersection< @@ -755,7 +776,11 @@ type ActionCreatorForCaseReducer = CR extends ( * * @internal */ -type SliceDefinedCaseReducers> = Id< +type SliceDefinedCaseReducers< + CaseReducers extends + | SliceCaseReducers + | Record, +> = Id< UnionToIntersection< SliceReducerCreators< any, @@ -800,7 +825,7 @@ type SliceDefinedSelectors< */ export type ValidateSliceCaseReducers< S, - ACR extends SliceCaseReducers, + ACR extends SliceCaseReducers | CreatorCallback, > = ACR & { [T in keyof ACR]: ACR[T] extends { reducer(s: S, action?: infer A): any @@ -962,7 +987,9 @@ export function buildCreateSlice< } return function createSlice< State, - CaseReducers extends SliceCaseReducers, + CaseReducers extends + | SliceCaseReducers + | CreatorCallback, Name extends string, Selectors extends SliceSelectors, ReducerPath extends string = Name, @@ -975,7 +1002,13 @@ export function buildCreateSlice< Selectors, CreatorMap >, - ): Slice { + ): Slice< + State, + GetCaseReducers, + Name, + ReducerPath, + Selectors + > { const { name, reducerPath = name as unknown as ReducerPath } = options if (!name) { throw new Error('`name` is a required option for createSlice') @@ -1056,7 +1089,7 @@ export function buildCreateSlice< reducerName, type: getType(name, reducerName), } - handler(reducerDetails, reducerDefinition, contextMethods) + handler(reducerDetails, reducerDefinition as any, contextMethods) } } else { for (const [reducerName, reducerDefinition] of Object.entries( @@ -1069,13 +1102,13 @@ export function buildCreateSlice< if ('reducer' in reducerDefinition) { preparedReducerCreator.handle( reducerDetails, - reducerDefinition, + reducerDefinition as any, contextMethods, ) } else { reducerCreator.handle( reducerDetails, - reducerDefinition, + reducerDefinition as any, contextMethods, ) } @@ -1142,7 +1175,13 @@ export function buildCreateSlice< reducerPath: CurrentReducerPath, injected = false, ): Pick< - Slice, + Slice< + State, + GetCaseReducers, + Name, + CurrentReducerPath, + Selectors + >, 'getSelectors' | 'selectors' | 'selectSlice' | 'reducerPath' > { function selectSlice(state: { [K in CurrentReducerPath]: State }) { @@ -1192,7 +1231,15 @@ export function buildCreateSlice< } } - const slice: Slice = { + const slice: Slice< + State, + CaseReducers extends CreatorCallback + ? ReturnType + : CaseReducers, + Name, + ReducerPath, + Selectors + > = { name, reducer, actions: context.actionCreators as any, diff --git a/packages/toolkit/src/entities/slice_creator.ts b/packages/toolkit/src/entities/slice_creator.ts index 86ad9e5e30..1fa3f3507f 100644 --- a/packages/toolkit/src/entities/slice_creator.ts +++ b/packages/toolkit/src/entities/slice_creator.ts @@ -1,8 +1,8 @@ import type { CaseReducer, + CreatorCaseReducers, ReducerCreator, ReducerCreatorEntry, - SliceCaseReducers, } from '@reduxjs/toolkit' import type { PayloadAction } from '../createAction' import { reducerCreator, type CaseReducerDefinition } from '../createSlice' @@ -96,7 +96,8 @@ type EntityMethodsCreator = declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [entityMethodsCreatorType]: ReducerCreatorEntry> diff --git a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts index df68d7c38e..59684bd207 100644 --- a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts +++ b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test-d.ts @@ -29,4 +29,45 @@ describe('entity slice creator', () => { PayloadActionCreator >() }) + it('can be used in object form, and shows error if incompatible', () => { + const bookAdapter = createEntityAdapter() + + const initialState = { data: bookAdapter.getInitialState() } + + const bookSlice = createAppSlice({ + name: 'books', + initialState: { data: bookAdapter.getInitialState() }, + // @ts-expect-error + reducers: { + ...entityMethodsCreator.create(bookAdapter), + }, + }) + + const bookSlice2 = createAppSlice({ + name: 'books', + initialState, + reducers: { + ...entityMethodsCreator.create(bookAdapter, { + // cannot be inferred, needs annotation + selectEntityState: (state: typeof initialState) => state.data, + }), + }, + }) + + expectTypeOf(bookSlice2.actions.addOne).toEqualTypeOf< + PayloadActionCreator + >() + + const bookSlice3 = createAppSlice({ + name: 'books', + initialState: bookAdapter.getInitialState(), + reducers: { + ...entityMethodsCreator.create(bookAdapter), + }, + }) + + expectTypeOf(bookSlice3.actions.addOne).toEqualTypeOf< + PayloadActionCreator + >() + }) }) diff --git a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts index a189b21733..d60897e720 100644 --- a/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts +++ b/packages/toolkit/src/entities/tests/entity_slice_enhancer.test.ts @@ -3,7 +3,6 @@ import { createEntityAdapter, createSlice, entityMethodsCreator, - preparedReducerCreator, } from '@reduxjs/toolkit' import type { PayloadAction, @@ -128,12 +127,25 @@ describe('entity slice creator', () => { initialState: bookAdapter.getInitialState(), reducers: { ...entityMethodsCreator.create(bookAdapter, { - // state can't be inferred here - selectEntityState: (state) => state as EntityState, name: 'book', }), }, }) expect(bookSlice.actions.addOneBook).toBeTypeOf('function') + + const initialState = { nested: bookAdapter.getInitialState() } + const nestedBookSlice = createAppSlice({ + name: 'book', + initialState, + reducers: { + ...entityMethodsCreator.create(bookAdapter, { + // state can't be inferred, so needs to be annotated + selectEntityState: (state: typeof initialState) => state.nested, + name: 'nestedBook', + pluralName: 'nestedBookies', + }), + }, + }) + expect(nestedBookSlice.actions.addOneNestedBook).toBeTypeOf('function') }) }) diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 9c35b9bd62..f875dcab42 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -77,6 +77,7 @@ export type { Slice, CaseReducerActions, SliceCaseReducers, + CreatorCaseReducers, ValidateSliceCaseReducers, CaseReducerWithPrepare, ReducerCreators, diff --git a/packages/toolkit/src/tests/createSlice.test.ts b/packages/toolkit/src/tests/createSlice.test.ts index afa82f6b45..28097bcbb6 100644 --- a/packages/toolkit/src/tests/createSlice.test.ts +++ b/packages/toolkit/src/tests/createSlice.test.ts @@ -5,6 +5,7 @@ import type { Action, CaseReducer, CaseReducerDefinition, + CreatorCaseReducers, PayloadAction, PayloadActionCreator, ReducerCreator, @@ -13,7 +14,6 @@ import type { ReducerDefinition, ReducerNamesOfType, SliceActionType, - SliceCaseReducers, ThunkAction, WithSlice, } from '@reduxjs/toolkit' @@ -910,6 +910,7 @@ describe('createSlice', () => { createSlice({ name: 'test', initialState: [] as any[], + // @ts-expect-error reducers: (create) => ({ prepared: { prepare: (p: string, m: number, e: { message: string }) => ({ @@ -917,7 +918,7 @@ describe('createSlice', () => { meta: m, error: e, }), - reducer: (state, action) => { + reducer: (state: any[], action: any) => { state.push(action) }, }, @@ -1237,7 +1238,8 @@ interface UndoableOptions { declare module '@reduxjs/toolkit' { export interface SliceReducerCreators< State = any, - CaseReducers extends SliceCaseReducers = SliceCaseReducers, + CaseReducers extends + CreatorCaseReducers = CreatorCaseReducers, Name extends string = string, > { [loaderCreatorType]: ReducerCreatorEntry<