Skip to content

Commit

Permalink
Fix type checking in reducers field by making callback into generic
Browse files Browse the repository at this point in the history
  • Loading branch information
EskiMojo14 committed Feb 4, 2024
1 parent 8adeb42 commit b7eb6b9
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 43 deletions.
29 changes: 19 additions & 10 deletions docs/api/createSlice.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,8 @@ const reducerCreatorType = Symbol()
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[reducerCreatorType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1144,7 +1145,8 @@ const batchedCreatorType = Symbol()
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[batchedCreatorType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1182,7 +1184,8 @@ const loaderCreatorType = Symbol()
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[loaderCreatorType]: ReducerCreatorEntry<
Expand All @@ -1208,7 +1211,8 @@ const loaderCreatorType = Symbol()
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[loaderCreatorType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1243,7 +1247,8 @@ const asyncThunkCreatorType = Symbol()
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[asyncThunkCreatorType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1283,7 +1288,8 @@ const preparedReducerType = Symbol()
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[preparedReducerType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1387,7 +1393,8 @@ interface ToastThunkCreator<
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[toastCreatorType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1516,7 +1523,8 @@ interface PaginationState {
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[paginationCreatorType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1586,7 +1594,7 @@ interface HistoryState<T> {
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string
> {
[paginationCreatorType]: ReducerCreatorEntry<
Expand Down Expand Up @@ -1695,7 +1703,8 @@ interface UndoableMeta {
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[undoableCreatorType]: ReducerCreatorEntry<
Expand Down
97 changes: 72 additions & 25 deletions packages/toolkit/src/createSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,13 @@ export type ReducerCreatorEntry<
: {}
}

export type CreatorCaseReducers<State> =
| Record<string, ReducerDefinition>
| SliceCaseReducers<State>

export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[ReducerType.reducer]: ReducerCreatorEntry<
Expand Down Expand Up @@ -315,7 +319,7 @@ export type ReducerCreator<Type extends RegisteredReducerType> = {
})

export type ReducerNamesOfType<
CaseReducers extends SliceCaseReducers<any>,
CaseReducers extends CreatorCaseReducers<any>,
Type extends RegisteredReducerType,
> = {
[ReducerName in keyof CaseReducers]: CaseReducers[ReducerName] extends ReducerDefinition<Type>
Expand All @@ -334,7 +338,9 @@ interface InjectIntoConfig<NewReducerPath extends string> extends InjectConfig {
*/
export interface Slice<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
| SliceCaseReducers<State>
| Record<string, ReducerDefinition> = SliceCaseReducers<State>,
Name extends string = string,
ReducerPath extends string = Name,
Selectors extends SliceSelectors<State> = SliceSelectors<State>,
Expand Down Expand Up @@ -422,7 +428,9 @@ export interface Slice<
*/
interface InjectedSlice<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
| SliceCaseReducers<State>
| Record<string, ReducerDefinition> = SliceCaseReducers<State>,
Name extends string = string,
ReducerPath extends string = Name,
Selectors extends SliceSelectors<State> = SliceSelectors<State>,
Expand Down Expand Up @@ -463,14 +471,29 @@ interface InjectedSlice<
selectSlice(state: { [K in ReducerPath]?: State | undefined }): State
}

type CreatorCallback<
State,
CreatorMap extends Record<string, RegisteredReducerType>,
> = (
create: ReducerCreators<State, CreatorMap>,
) => Record<string, ReducerDefinition>

type GetCaseReducers<
State,
CreatorMap extends Record<string, RegisteredReducerType>,
CR extends SliceCaseReducers<State> | CreatorCallback<State, CreatorMap>,
> = CR extends CreatorCallback<State, CreatorMap> ? ReturnType<CR> : CR

/**
* Options for `createSlice()`.
*
* @public
*/
export interface CreateSliceOptions<
State = any,
CR extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CR extends
| SliceCaseReducers<State>
| CreatorCallback<State, CreatorMap> = SliceCaseReducers<State>,
Name extends string = string,
ReducerPath extends string = Name,
Selectors extends SliceSelectors<State> = SliceSelectors<State>,
Expand All @@ -496,9 +519,7 @@ export interface CreateSliceOptions<
* functions. For every action type, a matching action creator will be
* generated using `createAction()`.
*/
reducers:
| ValidateSliceCaseReducers<State, CR>
| ((create: ReducerCreators<State, CreatorMap>) => CR)
reducers: ValidateSliceCaseReducers<State, CR>

/**
* A callback that receives a *builder* object to define
Expand Down Expand Up @@ -687,13 +708,11 @@ interface AsyncThunkCreator<
*
* @public
*/
export type SliceCaseReducers<State> =
| Record<string, ReducerDefinition>
| Record<
string,
| CaseReducer<State, PayloadAction<any>>
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
>
export type SliceCaseReducers<State> = Record<
string,
| CaseReducer<State, PayloadAction<any>>
| CaseReducerWithPrepare<State, PayloadAction<any, string, any, any>>
>

/**
* The type describing a slice's `selectors` option.
Expand All @@ -713,7 +732,9 @@ export type SliceActionType<
* @public
*/
export type CaseReducerActions<
CaseReducers extends SliceCaseReducers<any>,
CaseReducers extends
| SliceCaseReducers<any>
| Record<string, ReducerDefinition>,
SliceName extends string,
> = Id<
UnionToIntersection<
Expand Down Expand Up @@ -755,7 +776,11 @@ type ActionCreatorForCaseReducer<CR, Type extends string> = CR extends (
*
* @internal
*/
type SliceDefinedCaseReducers<CaseReducers extends SliceCaseReducers<any>> = Id<
type SliceDefinedCaseReducers<
CaseReducers extends
| SliceCaseReducers<any>
| Record<string, ReducerDefinition>,
> = Id<
UnionToIntersection<
SliceReducerCreators<
any,
Expand Down Expand Up @@ -800,7 +825,7 @@ type SliceDefinedSelectors<
*/
export type ValidateSliceCaseReducers<
S,
ACR extends SliceCaseReducers<S>,
ACR extends SliceCaseReducers<S> | CreatorCallback<S, any>,
> = ACR & {
[T in keyof ACR]: ACR[T] extends {
reducer(s: S, action?: infer A): any
Expand Down Expand Up @@ -962,7 +987,9 @@ export function buildCreateSlice<
}
return function createSlice<
State,
CaseReducers extends SliceCaseReducers<State>,
CaseReducers extends
| SliceCaseReducers<State>
| CreatorCallback<State, CreatorMap>,
Name extends string,
Selectors extends SliceSelectors<State>,
ReducerPath extends string = Name,
Expand All @@ -975,7 +1002,13 @@ export function buildCreateSlice<
Selectors,
CreatorMap
>,
): Slice<State, CaseReducers, Name, ReducerPath, Selectors> {
): Slice<
State,
GetCaseReducers<State, CreatorMap, CaseReducers>,
Name,
ReducerPath,
Selectors
> {
const { name, reducerPath = name as unknown as ReducerPath } = options
if (!name) {
throw new Error('`name` is a required option for createSlice')
Expand Down Expand Up @@ -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(
Expand All @@ -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,
)
}
Expand Down Expand Up @@ -1142,7 +1175,13 @@ export function buildCreateSlice<
reducerPath: CurrentReducerPath,
injected = false,
): Pick<
Slice<State, CaseReducers, Name, CurrentReducerPath, Selectors>,
Slice<
State,
GetCaseReducers<State, CreatorMap, CaseReducers>,
Name,
CurrentReducerPath,
Selectors
>,
'getSelectors' | 'selectors' | 'selectSlice' | 'reducerPath'
> {
function selectSlice(state: { [K in CurrentReducerPath]: State }) {
Expand Down Expand Up @@ -1192,7 +1231,15 @@ export function buildCreateSlice<
}
}

const slice: Slice<State, CaseReducers, Name, ReducerPath, Selectors> = {
const slice: Slice<
State,
CaseReducers extends CreatorCallback<State, CreatorMap>
? ReturnType<CaseReducers>
: CaseReducers,
Name,
ReducerPath,
Selectors
> = {
name,
reducer,
actions: context.actionCreators as any,
Expand Down
5 changes: 3 additions & 2 deletions packages/toolkit/src/entities/slice_creator.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -96,7 +96,8 @@ type EntityMethodsCreator<State> =
declare module '@reduxjs/toolkit' {
export interface SliceReducerCreators<
State = any,
CaseReducers extends SliceCaseReducers<State> = SliceCaseReducers<State>,
CaseReducers extends
CreatorCaseReducers<State> = CreatorCaseReducers<State>,
Name extends string = string,
> {
[entityMethodsCreatorType]: ReducerCreatorEntry<EntityMethodsCreator<State>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,45 @@ describe('entity slice creator', () => {
PayloadActionCreator<BookModel, 'books/addOne'>
>()
})
it('can be used in object form, and shows error if incompatible', () => {
const bookAdapter = createEntityAdapter<BookModel>()

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<BookModel, 'books/addOne'>
>()

const bookSlice3 = createAppSlice({
name: 'books',
initialState: bookAdapter.getInitialState(),
reducers: {
...entityMethodsCreator.create(bookAdapter),
},
})

expectTypeOf(bookSlice3.actions.addOne).toEqualTypeOf<
PayloadActionCreator<BookModel, 'books/addOne'>
>()
})
})
Loading

0 comments on commit b7eb6b9

Please sign in to comment.