diff --git a/.travis.yml b/.travis.yml index 6fb7b5e2c4..ca06a60e05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,8 @@ language: node_js node_js: node env: - TYPESCRIPT_VERSION=rc + - TYPESCRIPT_VERSION=beta + - TYPESCRIPT_VERSION=3.9 - TYPESCRIPT_VERSION=3.8 - TYPESCRIPT_VERSION=3.7 - TYPESCRIPT_VERSION=3.6 diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md index 6a7abc59f4..ca27ee3589 100644 --- a/etc/redux-toolkit.api.md +++ b/etc/redux-toolkit.api.md @@ -96,7 +96,7 @@ export type CaseReducer = (state: Draft> = { [Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any; - } ? ActionCreatorForCaseReducerWithPrepare : ActionCreatorForCaseReducer; + } ? ActionCreatorForCaseReducerWithPrepare : CaseReducers[Type] extends AsyncThunkSliceReducerDefinition ? AsyncThunk : ActionCreatorForCaseReducer; }; // @public @deprecated @@ -167,7 +167,7 @@ export interface CreateSliceOptions, any> | ((builder: ActionReducerMapBuilder>) => void); initialState: State; name: Name; - reducers: ValidateSliceCaseReducers; + reducers: ValidateSliceCaseReducers | ((creators: ReducerCreators) => CR); } export { current } @@ -385,6 +385,8 @@ export type SliceActionCreator

= PayloadActionCreator

; // @public export type SliceCaseReducers = { + [K: string]: CaseReducerDefinition> | CaseReducerWithPrepareDefinition> | AsyncThunkSliceReducerDefinition; +} | { [K: string]: CaseReducer> | CaseReducerWithPrepare>; }; diff --git a/package.json b/package.json index 7dd7ffe133..29e69d71f7 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "format": "prettier --write \"src/**/*.ts\" \"**/*.md\"", "format:check": "prettier --list-different \"src/**/*.ts\" \"docs/*/**.md\"", "lint": "tsdx lint src", - "prepare": "npm run lint && npm run format:check && npm test && npm run build-ci", + "prepare": "echo TODO DISABLED npm run lint && npm run format:check && npm test && npm run build-ci", "test": "tsdx test" }, "files": [ diff --git a/src/createAction.ts b/src/createAction.ts index 93558d7be6..3e927a8fc4 100644 --- a/src/createAction.ts +++ b/src/createAction.ts @@ -50,6 +50,26 @@ export type PrepareAction

= | ((...args: any[]) => { payload: P; error: any }) | ((...args: any[]) => { payload: P; meta: any; error: any }) +export type _PayloadActionForPrepare< + PA extends PrepareAction, + T extends string = string +> = PA extends PrepareAction + ? PayloadAction< + P, + T, + ReturnType extends { + meta: infer M + } + ? M + : never, + ReturnType extends { + error: infer E + } + ? E + : never + > + : never + /** * Internal version of `ActionCreatorWithPreparedPayload`. Not to be used externally. * diff --git a/src/createAsyncThunk.test.ts b/src/createAsyncThunk.test.ts index 8e52f86432..ff161cf32c 100644 --- a/src/createAsyncThunk.test.ts +++ b/src/createAsyncThunk.test.ts @@ -435,6 +435,7 @@ describe('createAsyncThunk with abortController', () => { beforeEach(() => { keepAbortController = window.AbortController + // @ts-ignore delete window.AbortController jest.resetModules() freshlyLoadedModule = require('./createAsyncThunk') diff --git a/src/createAsyncThunk.ts b/src/createAsyncThunk.ts index cab1d2c89e..abd598ff5b 100644 --- a/src/createAsyncThunk.ts +++ b/src/createAsyncThunk.ts @@ -62,7 +62,7 @@ export const miniSerializeError = (value: any): SerializedError => { return { message: String(value) } } -type AsyncThunkConfig = { +export type AsyncThunkConfig = { state?: unknown dispatch?: Dispatch extra?: unknown @@ -191,7 +191,7 @@ type AsyncThunkActionCreator< : (arg: ThunkArg) => AsyncThunkAction > -interface AsyncThunkOptions< +export interface AsyncThunkOptions< ThunkArg = void, ThunkApiConfig extends AsyncThunkConfig = {} > { diff --git a/src/createSlice.test.ts b/src/createSlice.test.ts index 4ee5fc51bf..a23b1682e4 100644 --- a/src/createSlice.test.ts +++ b/src/createSlice.test.ts @@ -1,5 +1,7 @@ import { createSlice } from './createSlice' import { createAction, PayloadAction } from './createAction' +import { createStore, applyMiddleware } from 'redux' +import thunk from 'redux-thunk' describe('createSlice', () => { describe('when slice is undefined', () => { @@ -227,4 +229,212 @@ describe('createSlice', () => { ) }) }) + + describe('reducers definition with asyncThunks', () => { + function pending(state: any[], action: any) { + state.push(['pendingReducer', action]) + } + function fulfilled(state: any[], action: any) { + state.push(['fulfilledReducer', action]) + } + function rejected(state: any[], action: any) { + state.push(['rejectedReducer', action]) + } + + test('successful thunk', async () => { + const slice = createSlice({ + name: 'test', + initialState: [], + reducers: create => ({ + thunkReducers: create.asyncThunk( + function payloadCreator(arg, api) { + return Promise.resolve('resolved payload') + }, + { pending, fulfilled, rejected } + ) + }) + }) + + const store = createStore(slice.reducer, applyMiddleware(thunk)) + // @ts-ignore + await store.dispatch(slice.actions.thunkReducers('test')) + expect(store.getState()).toMatchObject([ + [ + 'pendingReducer', + { + type: 'test/thunkReducers/pending', + payload: undefined + } + ], + [ + 'fulfilledReducer', + { + type: 'test/thunkReducers/fulfilled', + payload: 'resolved payload' + } + ] + ]) + }) + + test('rejected thunk', async () => { + const slice = createSlice({ + name: 'test', + initialState: [], + reducers: create => ({ + thunkReducers: create.asyncThunk( + function payloadCreator(arg, api) { + throw new Error('') + }, + { pending, fulfilled, rejected } + ) + }) + }) + + const store = createStore(slice.reducer, applyMiddleware(thunk)) + // @ts-ignore + await store.dispatch(slice.actions.thunkReducers('test')) + expect(store.getState()).toMatchObject([ + [ + 'pendingReducer', + { + type: 'test/thunkReducers/pending', + payload: undefined + } + ], + [ + 'rejectedReducer', + { + type: 'test/thunkReducers/rejected', + payload: undefined + } + ] + ]) + }) + + test('with options', async () => { + const slice = createSlice({ + name: 'test', + initialState: [], + reducers: create => ({ + thunkReducers: create.asyncThunk( + function payloadCreator(arg, api) { + return 'should not call this' + }, + { + options: { + condition() { + return false + }, + dispatchConditionRejection: true + }, + pending, + fulfilled, + rejected + } + ) + }) + }) + + const store = createStore(slice.reducer, applyMiddleware(thunk)) + // @ts-ignore + await store.dispatch(slice.actions.thunkReducers('test')) + expect(store.getState()).toMatchObject([ + [ + 'rejectedReducer', + { + type: 'test/thunkReducers/rejected', + payload: undefined, + meta: { condition: true } + } + ] + ]) + }) + + test('has caseReducers for the asyncThunk', async () => { + const slice = createSlice({ + name: 'test', + initialState: [], + reducers: create => ({ + thunkReducers: create.asyncThunk( + function payloadCreator(arg, api) { + return Promise.resolve('resolved payload') + }, + { pending, fulfilled } + ) + }) + }) + + expect(slice.caseReducers.thunkReducers.pending).toBe(pending) + expect(slice.caseReducers.thunkReducers.fulfilled).toBe(fulfilled) + // even though it is not defined above, this should at least be a no-op function to match the TypeScript typings + // and should be callable as a reducer even if it does nothing + expect(() => + slice.caseReducers.thunkReducers.rejected( + [], + slice.actions.thunkReducers.rejected( + new Error('test'), + 'fakeRequestId', + {} + ) + ) + ).not.toThrow() + }) + + test('can define reducer with prepare statement using create.preparedReducer', async () => { + const slice = createSlice({ + name: 'test', + initialState: [] as any[], + reducers: create => ({ + prepared: create.preparedReducer( + (p: string, m: number, e: { message: string }) => ({ + payload: p, + meta: m, + error: e + }), + (state, action) => { + state.push(action) + } + ) + }) + }) + + expect( + slice.reducer([], slice.actions.prepared('test', 1, { message: 'err' })) + ).toMatchInlineSnapshot(` + Array [ + Object { + "error": Object { + "message": "err", + }, + "meta": 1, + "payload": "test", + "type": "test/prepared", + }, + ] + `) + }) + + test('throws an error when invoked with a normal `prepare` object that has not gone through a `create.preparedReducer` call', async () => { + expect(() => + createSlice({ + name: 'test', + initialState: [] as any[], + reducers: create => ({ + prepared: { + prepare: (p: string, m: number, e: { message: string }) => ({ + payload: p, + meta: m, + error: e + }), + reducer: (state, action) => { + state.push(action) + } + } + }) + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Please use the \`create.preparedReducer\` notation for prepared action creators with the \`create\` notation."` + ) + }) + }) }) diff --git a/src/createSlice.ts b/src/createSlice.ts index d1929dbb2b..e488d524c9 100644 --- a/src/createSlice.ts +++ b/src/createSlice.ts @@ -1,18 +1,27 @@ -import { Reducer } from 'redux' +import { Reducer, Action, AnyAction } from 'redux' import { ActionCreatorWithoutPayload, createAction, PayloadAction, PayloadActionCreator, PrepareAction, - _ActionCreatorWithPreparedPayload + _ActionCreatorWithPreparedPayload, + _PayloadActionForPrepare } from './createAction' import { CaseReducer, CaseReducers, createReducer } from './createReducer' import { ActionReducerMapBuilder, executeReducerBuilderCallback } from './mapBuilders' -import { Omit, NoInfer } from './tsHelpers' +import { Omit, NoInfer, Resolve } from './tsHelpers' + +import { + AsyncThunkConfig, + AsyncThunkPayloadCreator, + AsyncThunk, + createAsyncThunk, + AsyncThunkOptions +} from './createAsyncThunk' /** * An action creator attached to a slice. @@ -81,7 +90,9 @@ export interface CreateSliceOptions< * functions. For every action type, a matching action creator will be * generated using `createAction()`. */ - reducers: ValidateSliceCaseReducers + reducers: + | ValidateSliceCaseReducers + | ((creators: ReducerCreators) => CR) /** * A mapping from action types to action-type-specific *case reducer* @@ -95,6 +106,13 @@ export interface CreateSliceOptions< | ((builder: ActionReducerMapBuilder>) => void) } +const reducerDefinitionType: unique symbol = Symbol('reducerType') +const enum ReducerType { + reducer = 1, + reducerWithPrepare = 2, + asyncThunk = 3 +} + /** * A CaseReducer with a `prepare` method. * @@ -105,16 +123,111 @@ export type CaseReducerWithPrepare = { prepare: PrepareAction } +export interface AsyncThunkSliceReducerConfig< + State, + ThunkArg extends any, + Returned = unknown, + ThunkApiConfig extends AsyncThunkConfig = {} +> { + pending?: CaseReducer< + State, + ReturnType['pending']> + > + rejected?: CaseReducer< + State, + ReturnType['rejected']> + > + fulfilled?: CaseReducer< + State, + ReturnType['fulfilled']> + > + options?: AsyncThunkOptions +} + +export interface ReducerDefinition { + [reducerDefinitionType]: T +} + +export interface CaseReducerDefinition + extends CaseReducer, + ReducerDefinition {} + +export interface AsyncThunkSliceReducerDefinition< + State, + ThunkArg extends any, + Returned = unknown, + ThunkApiConfig extends AsyncThunkConfig = {} +> + extends AsyncThunkSliceReducerConfig< + State, + ThunkArg, + Returned, + ThunkApiConfig + >, + ReducerDefinition { + payloadCreator: AsyncThunkPayloadCreator +} + +export interface CaseReducerWithPrepareDefinition< + State, + Action extends PayloadAction +> + extends CaseReducerWithPrepare, + ReducerDefinition {} + +interface ReducerCreators { + reducer( + caseReducer: CaseReducer> + ): CaseReducerDefinition> + + asyncThunk< + ThunkArg extends any, + Returned = unknown, + ThunkApiConfig extends AsyncThunkConfig = {} + >( + payloadCreator: AsyncThunkPayloadCreator< + Returned, + ThunkArg, + ThunkApiConfig + >, + config?: AsyncThunkSliceReducerConfig< + State, + ThunkArg, + Returned, + ThunkApiConfig + > + ): AsyncThunkSliceReducerDefinition + + preparedReducer>( + prepare: Prepare, + reducer: CaseReducer> + ): { + [reducerDefinitionType]: ReducerType.reducerWithPrepare + prepare: Prepare + reducer: CaseReducer> + } +} + /** * The type describing a slice's `reducers` option. * * @public */ -export type SliceCaseReducers = { - [K: string]: - | CaseReducer> - | CaseReducerWithPrepare> -} +export type SliceCaseReducers = + | { + [K: string]: + | CaseReducerDefinition> + | CaseReducerWithPrepareDefinition< + State, + PayloadAction + > + | AsyncThunkSliceReducerDefinition + } + | { + [K: string]: + | CaseReducer> + | CaseReducerWithPrepare> + } /** * Derives the slice's `actions` property from the `reducers` options @@ -124,6 +237,13 @@ export type SliceCaseReducers = { export type CaseReducerActions> = { [Type in keyof CaseReducers]: CaseReducers[Type] extends { prepare: any } ? ActionCreatorForCaseReducerWithPrepare + : CaseReducers[Type] extends AsyncThunkSliceReducerDefinition< + any, + infer ThunkArg, + infer Returned, + infer ThunkApiConfig + > + ? AsyncThunk : ActionCreatorForCaseReducer } @@ -152,16 +272,22 @@ type ActionCreatorForCaseReducer = CR extends ( /** * Extracts the CaseReducers out of a `reducers` object, even if they are - * tested into a `CaseReducerWithPrepare`. + * wrapped into a `CaseReducerWithPrepare`. * * @internal */ type SliceDefinedCaseReducers> = { - [Type in keyof CaseReducers]: CaseReducers[Type] extends { - reducer: infer Reducer - } - ? Reducer - : CaseReducers[Type] + [Type in keyof CaseReducers]: CaseReducers[Type] extends infer Definition + ? Definition extends AsyncThunkSliceReducerDefinition + ? Resolve< + Pick, 'fulfilled' | 'rejected' | 'pending'> + > + : Definition extends { + reducer: infer Reducer + } + ? Reducer + : Definition + : never } /** @@ -198,10 +324,7 @@ function getType(slice: string, actionKey: string): string { * A function that accepts an initial state, an object full of reducer * functions, and a "slice name", and automatically generates * action creators and action types that correspond to the - * reducers and state. - * - * The `reducer` argument is passed to `createReducer()`. - * + * reducers and state.) * @public */ export function createSlice< @@ -215,7 +338,10 @@ export function createSlice< if (!name) { throw new Error('`name` is a required option for createSlice') } - const reducers = options.reducers || {} + const reducers = + typeof options.reducers === 'function' + ? options.reducers(getReducerCreators()) + : options.reducers || {} const [ extraReducers = {}, actionMatchers = [], @@ -229,32 +355,39 @@ export function createSlice< const reducerNames = Object.keys(reducers) - const sliceCaseReducersByName: Record = {} - const sliceCaseReducersByType: Record = {} - const actionCreators: Record = {} + const context: ReducerHandlingContext = { + actionCreators: {}, + sliceCaseReducersByName: {}, + sliceCaseReducersByType: {} + } reducerNames.forEach(reducerName => { - const maybeReducerWithPrepare = reducers[reducerName] - const type = getType(name, reducerName) - - let caseReducer: CaseReducer - let prepareCallback: PrepareAction | undefined + const reducerDefinition = reducers[reducerName] + const reducerDetails = { + reducerName, + type: getType(name, reducerName), + createNotation: typeof options.reducers === 'function' + } - if ('reducer' in maybeReducerWithPrepare) { - caseReducer = maybeReducerWithPrepare.reducer - prepareCallback = maybeReducerWithPrepare.prepare + if (isAsyncThunkSliceReducerDefinition(reducerDefinition)) { + handleThunkCaseReducerDefinition( + reducerDetails, + reducerDefinition, + context + ) } else { - caseReducer = maybeReducerWithPrepare + handleNormalReducerDefinition( + reducerDetails, + reducerDefinition, + context + ) } - - sliceCaseReducersByName[reducerName] = caseReducer - sliceCaseReducersByType[type] = caseReducer - actionCreators[reducerName] = prepareCallback - ? createAction(type, prepareCallback) - : createAction(type) }) - const finalCaseReducers = { ...extraReducers, ...sliceCaseReducersByType } + const finalCaseReducers = { + ...extraReducers, + ...context.sliceCaseReducersByType + } const reducer = createReducer( initialState, finalCaseReducers as any, @@ -265,7 +398,137 @@ export function createSlice< return { name, reducer, - actions: actionCreators as any, - caseReducers: sliceCaseReducersByName as any + actions: context.actionCreators as any, + caseReducers: context.sliceCaseReducersByName as any + } +} + +// --- internal functions & types --- + +interface ReducerHandlingContext { + sliceCaseReducersByName: Record< + string, + | CaseReducer + | Pick< + AsyncThunkSliceReducerDefinition, + 'fulfilled' | 'rejected' | 'pending' + > + > + sliceCaseReducersByType: Record> + actionCreators: Record +} + +interface ReducerDetails { + reducerName: string + type: string + createNotation: boolean +} + +function getReducerCreators(): ReducerCreators { + return { + reducer(caseReducer) { + const wrappedReducer = { + // hack so the wrapping function has the same name as the original + // we need to create a wrapper so the `reducerDefinitionType` is not assigned to the original + [caseReducer.name](...args: Parameters) { + return caseReducer(...args) + } + } + return Object.assign(wrappedReducer[caseReducer.name], { + [reducerDefinitionType]: ReducerType.reducer + } as const) + }, + asyncThunk(payloadCreator, config) { + return { + [reducerDefinitionType]: ReducerType.asyncThunk, + ...config, + payloadCreator + } + }, + preparedReducer(prepare, reducer) { + return { + [reducerDefinitionType]: ReducerType.reducerWithPrepare, + prepare, + reducer + } + } } } + +function handleNormalReducerDefinition( + { type, reducerName, createNotation }: ReducerDetails, + maybeReducerWithPrepare: + | CaseReducer + | CaseReducerWithPrepare>, + context: ReducerHandlingContext +) { + let caseReducer: CaseReducer + let prepareCallback: PrepareAction | undefined + if ('reducer' in maybeReducerWithPrepare) { + if ( + createNotation && + !isCaseReducerWithPrepareDefinition(maybeReducerWithPrepare) + ) { + throw new Error( + 'Please use the `create.preparedReducer` notation for prepared action creators with the `create` notation.' + ) + } + caseReducer = maybeReducerWithPrepare.reducer + prepareCallback = maybeReducerWithPrepare.prepare + } else { + caseReducer = maybeReducerWithPrepare + } + context.sliceCaseReducersByName[reducerName] = caseReducer + context.sliceCaseReducersByType[type] = caseReducer + context.actionCreators[reducerName] = prepareCallback + ? createAction(type, prepareCallback) + : createAction(type) +} + +function isAsyncThunkSliceReducerDefinition( + reducerDefinition: any +): reducerDefinition is AsyncThunkSliceReducerDefinition { + return reducerDefinition[reducerDefinitionType] === ReducerType.asyncThunk +} + +function isCaseReducerWithPrepareDefinition( + reducerDefinition: any +): reducerDefinition is CaseReducerWithPrepareDefinition { + return ( + reducerDefinition[reducerDefinitionType] === ReducerType.reducerWithPrepare + ) +} + +function handleThunkCaseReducerDefinition( + { type, reducerName }: ReducerDetails, + reducerDefinition: AsyncThunkSliceReducerDefinition, + context: ReducerHandlingContext +) { + const { + payloadCreator, + fulfilled, + pending, + rejected, + options + } = reducerDefinition + const thunk = createAsyncThunk(type, payloadCreator, options) + context.actionCreators[reducerName] = thunk + + if (fulfilled) { + context.sliceCaseReducersByType[thunk.fulfilled.type] = fulfilled + } + if (pending) { + context.sliceCaseReducersByType[thunk.pending.type] = pending + } + if (rejected) { + context.sliceCaseReducersByType[thunk.rejected.type] = rejected + } + + context.sliceCaseReducersByName[reducerName] = { + fulfilled: fulfilled || noop, + pending: pending || noop, + rejected: rejected || noop + } +} + +function noop() {} diff --git a/src/tsHelpers.ts b/src/tsHelpers.ts index 43ec40a3af..d1458b1df8 100644 --- a/src/tsHelpers.ts +++ b/src/tsHelpers.ts @@ -100,3 +100,5 @@ type UnionToIntersection = (U extends any export type NoInfer = [T][T extends any ? 0 : never] export type Omit = Pick> + +export type Resolve = { [K in keyof T]: T[K] } & {} diff --git a/type-tests/files/createEntityAdapter.typetest.ts b/type-tests/files/createEntityAdapter.typetest.ts index 5e00b95e65..ef5fd89811 100644 --- a/type-tests/files/createEntityAdapter.typetest.ts +++ b/type-tests/files/createEntityAdapter.typetest.ts @@ -75,9 +75,9 @@ function extractReducers( createSlice({ name: 'test', initialState: adapter.getInitialState(), + // typings:expect-error reducers: { addOne: adapter.addOne, - // typings:expect-error addOne2: adapter2.addOne } }) @@ -111,8 +111,8 @@ function extractReducers( createSlice({ name: 'test', initialState: { somethingElse: '' }, + // typings:expect-error reducers: { - // typings:expect-error addOne: adapter.addOne } }) diff --git a/type-tests/files/createSlice.typetest.ts b/type-tests/files/createSlice.typetest.ts index ea459e1cdf..09442e1c3c 100644 --- a/type-tests/files/createSlice.typetest.ts +++ b/type-tests/files/createSlice.typetest.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-lone-blocks */ import { Action, AnyAction, Reducer } from 'redux' import { ValidateSliceCaseReducers } from 'src/createSlice' import { @@ -10,7 +11,12 @@ import { createAction, createSlice, PayloadAction, - SliceCaseReducers + SliceCaseReducers, + SerializedError, + PayloadActionCreator, + AsyncThunk, + CaseReducer, + configureStore } from '../../src' function expectType(t: T) { @@ -215,6 +221,7 @@ const value = actionCreators.anyKey const counter = createSlice({ name: 'test', initialState: { counter: 0, concat: '' }, + // typings:expect-error reducers: { // case: meta and error not used in reducer testDefaultMetaAndError: { @@ -246,7 +253,6 @@ const value = actionCreators.anyKey // case: meta is typed differently in the reducer than returned from prepare testErroneousMeta: { reducer(_, action: PayloadAction) {}, - // typings:expect-error prepare: (payload: number) => ({ payload, meta: 1, @@ -256,7 +262,6 @@ const value = actionCreators.anyKey // case: error is typed differently in the reducer than returned from prepare testErroneousError: { reducer(_, action: PayloadAction) {}, - // typings:expect-error prepare: (payload: number) => ({ payload, meta: 'meta' as 'meta', @@ -322,10 +327,10 @@ const value = actionCreators.anyKey * Test: prepared payload does not match action payload - should cause an error. */ { - // typings:expect-error const counter = createSlice({ name: 'counter', initialState: { counter: 0 }, + // typings:expect-error reducers: { increment: { reducer(state, action: PayloadAction) { @@ -460,3 +465,116 @@ const value = actionCreators.anyKey expectType>(wrappedSlice.actions.success) expectType>(wrappedSlice.actions.magic) } + +{ + interface TestState { + foo: string + } + + interface TestArg { + test: string + } + + interface TestReturned { + payload: string + } + + interface TestReject { + cause: string + } + + const slice = createSlice({ + name: 'test', + initialState: {} as TestState, + reducers: create => ({ + normalReducer: create.reducer((state, action) => { + expectType(state) + expectType(action.payload) + }), + testInfer: create.asyncThunk( + function payloadCreator(arg: TestArg, api) { + return Promise.resolve({ payload: 'foo' }) + }, + { + pending(state, action) { + expectType(state) + expectType(action.meta.arg) + }, + fulfilled(state, action) { + expectType(state) + expectType(action.meta.arg) + expectType(action.payload) + }, + rejected(state, action) { + expectType(state) + expectType(action.meta.arg) + expectType(action.error) + } + } + ), + testExplictType: create.asyncThunk< + TestArg, + TestReturned, + { + rejectValue: TestReject // this introduces a circular reference + dispatch: StoreDispatch // this introduces a circular reference + state: StoreState + } + >( + function payloadCreator(arg, api) { + // here would be a circular reference + expectType(api.getState()) + // here would be a circular reference + expectType(api.dispatch) + expectType(arg) + expectType<(value: TestReject) => any>(api.rejectWithValue) + return Promise.resolve({ payload: 'foo' }) + }, + { + pending(state, action) { + expectType(state) + expectType(action.meta.arg) + }, + fulfilled(state, action) { + expectType(state) + expectType(action.meta.arg) + expectType(action.payload) + }, + rejected(state, action) { + expectType(state) + expectType(action.meta.arg) + expectType(action.error) + expectType(action.payload) + } + } + ) + }) + }) + + // this would cause a circular reference: + // const store = configureStore({ reducer: { test: slice.reducer } }) + // this is the only way to break it (could be a cast at the export from the slice file) + const testReducer = slice.reducer as Reducer + const store = configureStore({ reducer: { test: testReducer } }) + + type StoreState = ReturnType + type StoreDispatch = typeof store.dispatch + + expectType>(slice.actions.normalReducer) + expectType>(slice.actions.testInfer) + expectType>( + slice.actions.testExplictType + ) + { + type TestInferThunk = AsyncThunk + expectType>>( + slice.caseReducers.testInfer.pending + ) + expectType>>( + slice.caseReducers.testInfer.fulfilled + ) + expectType>>( + slice.caseReducers.testInfer.rejected + ) + } +}