From 8e82f4e91da12262ae01222377f1ae2754ce28e1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 16 Aug 2022 15:52:19 +0200 Subject: [PATCH 1/5] add createAsyncThunk.forTypes --- packages/toolkit/src/createAsyncThunk.ts | 495 ++++++++++-------- .../src/tests/createAsyncThunk.typetest.ts | 59 ++- 2 files changed, 322 insertions(+), 232 deletions(-) diff --git a/packages/toolkit/src/createAsyncThunk.ts b/packages/toolkit/src/createAsyncThunk.ts index f2ece1f0f1..937ac6f15c 100644 --- a/packages/toolkit/src/createAsyncThunk.ts +++ b/packages/toolkit/src/createAsyncThunk.ts @@ -5,7 +5,7 @@ import type { } from './createAction' import { createAction } from './createAction' import type { ThunkDispatch } from 'redux-thunk' -import type { FallbackIfUnknown, IsAny, IsUnknown } from './tsHelpers' +import type { FallbackIfUnknown, Id, IsAny, IsUnknown } from './tsHelpers' import { nanoid } from './nanoid' // @ts-ignore we need the import of these types due to a bundling issue. @@ -416,269 +416,302 @@ export type AsyncThunk< typePrefix: string } -/** - * - * @param typePrefix - * @param payloadCreator - * @param options - * - * @public - */ -// separate signature without `AsyncThunkConfig` for better inference -export function createAsyncThunk( - typePrefix: string, - payloadCreator: AsyncThunkPayloadCreator, - options?: AsyncThunkOptions -): AsyncThunk +type OverrideThunkApiConfigs = Id< + NewConfig & Omit +> -/** - * - * @param typePrefix - * @param payloadCreator - * @param options - * - * @public - */ -export function createAsyncThunk< - Returned, - ThunkArg, - ThunkApiConfig extends AsyncThunkConfig ->( - typePrefix: string, - payloadCreator: AsyncThunkPayloadCreator, - options?: AsyncThunkOptions -): AsyncThunk +type CreateAsyncThunk = { + /** + * + * @param typePrefix + * @param payloadCreator + * @param options + * + * @public + */ + // separate signature without `AsyncThunkConfig` for better inference + ( + typePrefix: string, + payloadCreator: AsyncThunkPayloadCreator< + Returned, + ThunkArg, + CurriedThunkApiConfig + >, + options?: AsyncThunkOptions + ): AsyncThunk -export function createAsyncThunk< - Returned, - ThunkArg, - ThunkApiConfig extends AsyncThunkConfig ->( - typePrefix: string, - payloadCreator: AsyncThunkPayloadCreator, - options?: AsyncThunkOptions -): AsyncThunk { - type RejectedValue = GetRejectValue - type PendingMeta = GetPendingMeta - type FulfilledMeta = GetFulfilledMeta - type RejectedMeta = GetRejectedMeta - - const fulfilled: AsyncThunkFulfilledActionCreator< + /** + * + * @param typePrefix + * @param payloadCreator + * @param options + * + * @public + */ + ( + typePrefix: string, + payloadCreator: AsyncThunkPayloadCreator< + Returned, + ThunkArg, + OverrideThunkApiConfigs + >, + options?: AsyncThunkOptions< + ThunkArg, + OverrideThunkApiConfigs + > + ): AsyncThunk< Returned, ThunkArg, - ThunkApiConfig - > = createAction( - typePrefix + '/fulfilled', - ( - payload: Returned, - requestId: string, - arg: ThunkArg, - meta?: FulfilledMeta - ) => ({ - payload, - meta: { - ...((meta as any) || {}), - arg, - requestId, - requestStatus: 'fulfilled' as const, - }, - }) - ) + OverrideThunkApiConfigs + > - const pending: AsyncThunkPendingActionCreator = - createAction( - typePrefix + '/pending', - (requestId: string, arg: ThunkArg, meta?: PendingMeta) => ({ - payload: undefined, - meta: { - ...((meta as any) || {}), - arg, - requestId, - requestStatus: 'pending' as const, - }, - }) - ) + withTypes(): CreateAsyncThunk< + OverrideThunkApiConfigs + > +} - const rejected: AsyncThunkRejectedActionCreator = - createAction( - typePrefix + '/rejected', +export const createAsyncThunk = (() => { + function createAsyncThunk< + Returned, + ThunkArg, + ThunkApiConfig extends AsyncThunkConfig + >( + typePrefix: string, + payloadCreator: AsyncThunkPayloadCreator< + Returned, + ThunkArg, + ThunkApiConfig + >, + options?: AsyncThunkOptions + ): AsyncThunk { + type RejectedValue = GetRejectValue + type PendingMeta = GetPendingMeta + type FulfilledMeta = GetFulfilledMeta + type RejectedMeta = GetRejectedMeta + + const fulfilled: AsyncThunkFulfilledActionCreator< + Returned, + ThunkArg, + ThunkApiConfig + > = createAction( + typePrefix + '/fulfilled', ( - error: Error | null, + payload: Returned, requestId: string, arg: ThunkArg, - payload?: RejectedValue, - meta?: RejectedMeta + meta?: FulfilledMeta ) => ({ payload, - error: ((options && options.serializeError) || miniSerializeError)( - error || 'Rejected' - ) as GetSerializedErrorType, meta: { ...((meta as any) || {}), arg, requestId, - rejectedWithValue: !!payload, - requestStatus: 'rejected' as const, - aborted: error?.name === 'AbortError', - condition: error?.name === 'ConditionError', + requestStatus: 'fulfilled' as const, }, }) ) - let displayedWarning = false - - const AC = - typeof AbortController !== 'undefined' - ? AbortController - : class implements AbortController { - signal = { - aborted: false, - addEventListener() {}, - dispatchEvent() { - return false - }, - onabort() {}, - removeEventListener() {}, - reason: undefined, - throwIfAborted() {}, - } - abort() { - if (process.env.NODE_ENV !== 'production') { - if (!displayedWarning) { - displayedWarning = true - console.info( - `This platform does not implement AbortController. + const pending: AsyncThunkPendingActionCreator = + createAction( + typePrefix + '/pending', + (requestId: string, arg: ThunkArg, meta?: PendingMeta) => ({ + payload: undefined, + meta: { + ...((meta as any) || {}), + arg, + requestId, + requestStatus: 'pending' as const, + }, + }) + ) + + const rejected: AsyncThunkRejectedActionCreator = + createAction( + typePrefix + '/rejected', + ( + error: Error | null, + requestId: string, + arg: ThunkArg, + payload?: RejectedValue, + meta?: RejectedMeta + ) => ({ + payload, + error: ((options && options.serializeError) || miniSerializeError)( + error || 'Rejected' + ) as GetSerializedErrorType, + meta: { + ...((meta as any) || {}), + arg, + requestId, + rejectedWithValue: !!payload, + requestStatus: 'rejected' as const, + aborted: error?.name === 'AbortError', + condition: error?.name === 'ConditionError', + }, + }) + ) + + let displayedWarning = false + + const AC = + typeof AbortController !== 'undefined' + ? AbortController + : class implements AbortController { + signal = { + aborted: false, + addEventListener() {}, + dispatchEvent() { + return false + }, + onabort() {}, + removeEventListener() {}, + reason: undefined, + throwIfAborted() {}, + } + abort() { + if (process.env.NODE_ENV !== 'production') { + if (!displayedWarning) { + displayedWarning = true + console.info( + `This platform does not implement AbortController. If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'.` - ) + ) + } } } } - } - function actionCreator( - arg: ThunkArg - ): AsyncThunkAction { - return (dispatch, getState, extra) => { - const requestId = options?.idGenerator - ? options.idGenerator(arg) - : nanoid() - - const abortController = new AC() - let abortReason: string | undefined - - const abortedPromise = new Promise((_, reject) => - abortController.signal.addEventListener('abort', () => - reject({ name: 'AbortError', message: abortReason || 'Aborted' }) + function actionCreator( + arg: ThunkArg + ): AsyncThunkAction { + return (dispatch, getState, extra) => { + const requestId = options?.idGenerator + ? options.idGenerator(arg) + : nanoid() + + const abortController = new AC() + let abortReason: string | undefined + + const abortedPromise = new Promise((_, reject) => + abortController.signal.addEventListener('abort', () => + reject({ name: 'AbortError', message: abortReason || 'Aborted' }) + ) ) - ) - let started = false - function abort(reason?: string) { - if (started) { - abortReason = reason - abortController.abort() + let started = false + function abort(reason?: string) { + if (started) { + abortReason = reason + abortController.abort() + } } - } - const promise = (async function () { - let finalAction: ReturnType - try { - let conditionResult = options?.condition?.(arg, { getState, extra }) - if (isThenable(conditionResult)) { - conditionResult = await conditionResult - } - if (conditionResult === false) { - // eslint-disable-next-line no-throw-literal - throw { - name: 'ConditionError', - message: 'Aborted due to condition callback returning false.', + const promise = (async function () { + let finalAction: ReturnType + try { + let conditionResult = options?.condition?.(arg, { getState, extra }) + if (isThenable(conditionResult)) { + conditionResult = await conditionResult } - } - started = true - dispatch( - pending( - requestId, - arg, - options?.getPendingMeta?.({ requestId, arg }, { getState, extra }) - ) - ) - finalAction = await Promise.race([ - abortedPromise, - Promise.resolve( - payloadCreator(arg, { - dispatch, - getState, - extra, - requestId, - signal: abortController.signal, - abort, - rejectWithValue: (( - value: RejectedValue, - meta?: RejectedMeta - ) => { - return new RejectWithValue(value, meta) - }) as any, - fulfillWithValue: ((value: unknown, meta?: FulfilledMeta) => { - return new FulfillWithMeta(value, meta) - }) as any, - }) - ).then((result) => { - if (result instanceof RejectWithValue) { - throw result - } - if (result instanceof FulfillWithMeta) { - return fulfilled(result.payload, requestId, arg, result.meta) + if (conditionResult === false) { + // eslint-disable-next-line no-throw-literal + throw { + name: 'ConditionError', + message: 'Aborted due to condition callback returning false.', } - return fulfilled(result as any, requestId, arg) - }), - ]) - } catch (err) { - finalAction = - err instanceof RejectWithValue - ? rejected(null, requestId, arg, err.payload, err.meta) - : rejected(err as any, requestId, arg) - } - // We dispatch the result action _after_ the catch, to avoid having any errors - // here get swallowed by the try/catch block, - // per https://twitter.com/dan_abramov/status/770914221638942720 - // and https://github.com/reduxjs/redux-toolkit/blob/e85eb17b39a2118d859f7b7746e0f3fee523e089/docs/tutorials/advanced-tutorial.md#async-error-handling-logic-in-thunks - - const skipDispatch = - options && - !options.dispatchConditionRejection && - rejected.match(finalAction) && - (finalAction as any).meta.condition - - if (!skipDispatch) { - dispatch(finalAction) - } - return finalAction - })() - return Object.assign(promise as Promise, { - abort, - requestId, - arg, - unwrap() { - return promise.then(unwrapResult) - }, - }) + } + started = true + dispatch( + pending( + requestId, + arg, + options?.getPendingMeta?.( + { requestId, arg }, + { getState, extra } + ) + ) + ) + finalAction = await Promise.race([ + abortedPromise, + Promise.resolve( + payloadCreator(arg, { + dispatch, + getState, + extra, + requestId, + signal: abortController.signal, + abort, + rejectWithValue: (( + value: RejectedValue, + meta?: RejectedMeta + ) => { + return new RejectWithValue(value, meta) + }) as any, + fulfillWithValue: ((value: unknown, meta?: FulfilledMeta) => { + return new FulfillWithMeta(value, meta) + }) as any, + }) + ).then((result) => { + if (result instanceof RejectWithValue) { + throw result + } + if (result instanceof FulfillWithMeta) { + return fulfilled(result.payload, requestId, arg, result.meta) + } + return fulfilled(result as any, requestId, arg) + }), + ]) + } catch (err) { + finalAction = + err instanceof RejectWithValue + ? rejected(null, requestId, arg, err.payload, err.meta) + : rejected(err as any, requestId, arg) + } + // We dispatch the result action _after_ the catch, to avoid having any errors + // here get swallowed by the try/catch block, + // per https://twitter.com/dan_abramov/status/770914221638942720 + // and https://github.com/reduxjs/redux-toolkit/blob/e85eb17b39a2118d859f7b7746e0f3fee523e089/docs/tutorials/advanced-tutorial.md#async-error-handling-logic-in-thunks + + const skipDispatch = + options && + !options.dispatchConditionRejection && + rejected.match(finalAction) && + (finalAction as any).meta.condition + + if (!skipDispatch) { + dispatch(finalAction) + } + return finalAction + })() + return Object.assign(promise as Promise, { + abort, + requestId, + arg, + unwrap() { + return promise.then(unwrapResult) + }, + }) + } } + + return Object.assign( + actionCreator as AsyncThunkActionCreator< + Returned, + ThunkArg, + ThunkApiConfig + >, + { + pending, + rejected, + fulfilled, + typePrefix, + } + ) } + createAsyncThunk.withTypes = createAsyncThunk as unknown - return Object.assign( - actionCreator as AsyncThunkActionCreator< - Returned, - ThunkArg, - ThunkApiConfig - >, - { - pending, - rejected, - fulfilled, - typePrefix, - } - ) -} + return createAsyncThunk as CreateAsyncThunk +})() interface UnwrappableAction { payload: any diff --git a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts index 98cfa93a1f..93db4c3028 100644 --- a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts +++ b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts @@ -1,6 +1,12 @@ /* eslint-disable no-lone-blocks */ import type { AnyAction, SerializedError, AsyncThunk } from '@reduxjs/toolkit' -import { createAsyncThunk, createReducer, unwrapResult } from '@reduxjs/toolkit' +import { + createAsyncThunk, + createReducer, + unwrapResult, + createSlice, + configureStore, +} from '@reduxjs/toolkit' import type { ThunkDispatch } from 'redux-thunk' import type { AxiosError } from 'axios' @@ -590,3 +596,54 @@ const anyAction = { type: 'foo' } as AnyAction async (_, api) => api.rejectWithValue(5, '') ) } + +{ + const typedCAT = createAsyncThunk.forTypes<{ + state: RootState + dispatch: AppDispatch + }>() + + const thunk = typedCAT('foo', (arg: number, api) => { + // correct getState Type + const test1: number = api.getState().foo.value + // correct dispatch type + const test2: number = api.dispatch( + (dispatch, getState) => getState().foo.value + ) + return test1 + test2 + }) + + const thunk2 = typedCAT('foo', (arg, api) => { + // correct getState Type + const test1: number = api.getState().foo.value + // correct dispatch type + const test2: number = api.dispatch( + (dispatch, getState) => getState().foo.value + ) + return test1 + test2 + }) + + const slice = createSlice({ + name: 'foo', + initialState: { value: 0 }, + reducers: {}, + extraReducers(builder) { + builder + .addCase(thunk.fulfilled, (state, action) => { + state.value += action.payload + }) + .addCase(thunk2.fulfilled, (state, action) => { + state.value += action.payload + }) + }, + }) + + const store = configureStore({ + reducer: { + foo: slice.reducer, + }, + }) + + type RootState = ReturnType + type AppDispatch = typeof store.dispatch +} From 17befa63c7cb6b252248174219cccee35522f333 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 16 Aug 2022 16:09:08 +0200 Subject: [PATCH 2/5] add another type test documenting still incomplete behaviour --- .../src/tests/createAsyncThunk.typetest.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts index 93db4c3028..33d3100014 100644 --- a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts +++ b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts @@ -623,6 +623,29 @@ const anyAction = { type: 'foo' } as AnyAction return test1 + test2 }) + const thunk3 = typedCAT< + number, + string, + // @ts-expect-error TODO + // right now this still errors because + // it does not contain `state` and `dispatch` + { + rejectValue: string + } + >('foo', (arg, api) => { + // correct getState Type + const test1: number = api.getState().foo.value + // correct dispatch type + const test2: number = api.dispatch( + (dispatch, getState) => getState().foo.value + ) + if (1 < 2) { + // TODO: @ts-expect-error + return api.rejectWithValue(5) + } + return api.rejectWithValue(5) + }) + const slice = createSlice({ name: 'foo', initialState: { value: 0 }, @@ -635,6 +658,10 @@ const anyAction = { type: 'foo' } as AnyAction .addCase(thunk2.fulfilled, (state, action) => { state.value += action.payload }) + .addCase(thunk3.rejected, (state, action) => { + // @ts-expect-error TODO does not have the right type yet because the config was incomplete + state.value += action.payload + }) }, }) From b27f404b57fd868d437ccb35c7c666e36a933977 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 19 Aug 2022 06:01:00 +0200 Subject: [PATCH 3/5] rename to `withTypes`, allow merge-overriding --- .../src/tests/createAsyncThunk.typetest.ts | 92 ++++++++++++------- packages/toolkit/src/tsHelpers.ts | 2 + 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts index 33d3100014..ff1151e407 100644 --- a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts +++ b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts @@ -12,12 +12,13 @@ import type { ThunkDispatch } from 'redux-thunk' import type { AxiosError } from 'axios' import apiRequest from 'axios' import type { IsAny, IsUnknown } from '@internal/tsHelpers' -import { expectType } from './helpers' +import { expectExactType, expectType } from './helpers' import type { AsyncThunkFulfilledActionCreator, AsyncThunkRejectedActionCreator, } from '@internal/createAsyncThunk' +const ANY = {} as any const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction> const anyAction = { type: 'foo' } as AnyAction @@ -598,53 +599,74 @@ const anyAction = { type: 'foo' } as AnyAction } { - const typedCAT = createAsyncThunk.forTypes<{ + const typedCAT = createAsyncThunk.withTypes<{ state: RootState dispatch: AppDispatch + rejectValue: string }>() + // inferred usage const thunk = typedCAT('foo', (arg: number, api) => { // correct getState Type const test1: number = api.getState().foo.value // correct dispatch type - const test2: number = api.dispatch( - (dispatch, getState) => getState().foo.value - ) + const test2: number = api.dispatch((dispatch, getState) => { + expectExactType< + ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction> + >(ANY)(dispatch) + expectExactType<() => { foo: { value: number } }>(ANY)(getState) + return getState().foo.value + }) + + if (1 < 2) + // @ts-expect-error + return api.rejectWithValue(5) + if (1 < 2) return api.rejectWithValue('test') return test1 + test2 }) + // usage with two generics const thunk2 = typedCAT('foo', (arg, api) => { + expectExactType('' as string)(arg) // correct getState Type const test1: number = api.getState().foo.value // correct dispatch type - const test2: number = api.dispatch( - (dispatch, getState) => getState().foo.value - ) + const test2: number = api.dispatch((dispatch, getState) => { + expectExactType< + ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction> + >(ANY)(dispatch) + expectExactType<() => { foo: { value: number } }>(ANY)(getState) + return getState().foo.value + }) + if (1 < 2) + // @ts-expect-error + return api.rejectWithValue(5) + if (1 < 2) return api.rejectWithValue('test') return test1 + test2 }) - const thunk3 = typedCAT< - number, - string, - // @ts-expect-error TODO - // right now this still errors because - // it does not contain `state` and `dispatch` - { - rejectValue: string - } - >('foo', (arg, api) => { - // correct getState Type - const test1: number = api.getState().foo.value - // correct dispatch type - const test2: number = api.dispatch( - (dispatch, getState) => getState().foo.value - ) - if (1 < 2) { - // TODO: @ts-expect-error - return api.rejectWithValue(5) + // usage with config override generic + const thunk3 = typedCAT( + 'foo', + (arg, api) => { + expectExactType('' as string)(arg) + // correct getState Type + const test1: number = api.getState().foo.value + // correct dispatch type + const test2: number = api.dispatch((dispatch, getState) => { + expectExactType< + ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction> + >(ANY)(dispatch) + expectExactType<() => { foo: { value: number } }>(ANY)(getState) + return getState().foo.value + }) + if (1 < 2) return api.rejectWithValue(5) + if (1 < 2) + // @ts-expect-error + return api.rejectWithValue('test') + return 5 } - return api.rejectWithValue(5) - }) + ) const slice = createSlice({ name: 'foo', @@ -655,13 +677,21 @@ const anyAction = { type: 'foo' } as AnyAction .addCase(thunk.fulfilled, (state, action) => { state.value += action.payload }) + .addCase(thunk.rejected, (state, action) => { + expectExactType('' as string | undefined)(action.payload) + }) .addCase(thunk2.fulfilled, (state, action) => { state.value += action.payload }) - .addCase(thunk3.rejected, (state, action) => { - // @ts-expect-error TODO does not have the right type yet because the config was incomplete + .addCase(thunk2.rejected, (state, action) => { + expectExactType('' as string | undefined)(action.payload) + }) + .addCase(thunk3.fulfilled, (state, action) => { state.value += action.payload }) + .addCase(thunk3.rejected, (state, action) => { + expectExactType(0 as number | undefined)(action.payload) + }) }, }) diff --git a/packages/toolkit/src/tsHelpers.ts b/packages/toolkit/src/tsHelpers.ts index 2e9f75c592..916f0dbd26 100644 --- a/packages/toolkit/src/tsHelpers.ts +++ b/packages/toolkit/src/tsHelpers.ts @@ -139,3 +139,5 @@ export type ActionFromMatcher> = M extends Matcher< > ? T : never + +export type Id = { [K in keyof T]: T[K] } & {} From 74e7cb4da95af125810dad303caafa6321c5b076 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Fri, 19 Aug 2022 15:21:23 -0400 Subject: [PATCH 4/5] Add checks for `extra` in `withTypes --- .../src/tests/createAsyncThunk.typetest.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts index ff1151e407..369b6b34af 100644 --- a/packages/toolkit/src/tests/createAsyncThunk.typetest.ts +++ b/packages/toolkit/src/tests/createAsyncThunk.typetest.ts @@ -603,6 +603,7 @@ const anyAction = { type: 'foo' } as AnyAction state: RootState dispatch: AppDispatch rejectValue: string + extra: { s: string; n: number } }>() // inferred usage @@ -618,6 +619,11 @@ const anyAction = { type: 'foo' } as AnyAction return getState().foo.value }) + // correct extra type + const { s, n } = api.extra + expectExactType(s) + expectExactType(n) + if (1 < 2) // @ts-expect-error return api.rejectWithValue(5) @@ -638,6 +644,11 @@ const anyAction = { type: 'foo' } as AnyAction expectExactType<() => { foo: { value: number } }>(ANY)(getState) return getState().foo.value }) + // correct extra type + const { s, n } = api.extra + expectExactType(s) + expectExactType(n) + if (1 < 2) // @ts-expect-error return api.rejectWithValue(5) @@ -660,6 +671,10 @@ const anyAction = { type: 'foo' } as AnyAction expectExactType<() => { foo: { value: number } }>(ANY)(getState) return getState().foo.value }) + // correct extra type + const { s, n } = api.extra + expectExactType(s) + expectExactType(n) if (1 < 2) return api.rejectWithValue(5) if (1 < 2) // @ts-expect-error From daf9920ad9033565426c02fe1e99892bb80acf99 Mon Sep 17 00:00:00 2001 From: Mark Erikson Date: Fri, 19 Aug 2022 15:26:04 -0400 Subject: [PATCH 5/5] Add example of `createAsyncThunk.withTypes` --- docs/usage/usage-with-typescript.md | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/usage/usage-with-typescript.md b/docs/usage/usage-with-typescript.md index 744b4075cf..1b123f2ba7 100644 --- a/docs/usage/usage-with-typescript.md +++ b/docs/usage/usage-with-typescript.md @@ -62,19 +62,18 @@ export type RootState = ReturnType export default store ``` -If you pass the reducers directly to `configureStore()` and do not define the root reducer explicitly, there is no reference to `rootReducer`. +If you pass the reducers directly to `configureStore()` and do not define the root reducer explicitly, there is no reference to `rootReducer`. Instead, you can refer to `store.getState`, in order to get the `State` type. ```typescript import { configureStore } from '@reduxjs/toolkit' import rootReducer from './rootReducer' const store = configureStore({ - reducer: rootReducer + reducer: rootReducer, }) export type RootState = ReturnType ``` - ### Getting the `Dispatch` type If you want to get the `Dispatch` type from your store, you can extract it after creating the store. It is recommended to give the type a different name like `AppDispatch` to prevent confusion, as the type name `Dispatch` is usually overused. You may also find it to be more convenient to export a hook like `useAppDispatch` shown below, then using it wherever you'd call `useDispatch`. @@ -489,6 +488,8 @@ const wrappedSlice = createGenericSlice({ ## `createAsyncThunk` +### Basic `createAsyncThunk` Types + In the most common use cases, you should not need to explicitly declare any types for the `createAsyncThunk` call itself. Just provide a type for the first argument to the `payloadCreator` argument as you would for any function argument, and the resulting thunk will accept the same type as its input parameter. @@ -518,8 +519,12 @@ const fetchUserById = createAsyncThunk( const lastReturnedAction = await store.dispatch(fetchUserById(3)) ``` +### Typing the `thunkApi` Object + The second argument to the `payloadCreator`, known as `thunkApi`, is an object containing references to the `dispatch`, `getState`, and `extra` arguments from the thunk middleware as well as a utility function called `rejectWithValue`. If you want to use these from within the `payloadCreator`, you will need to define some generic arguments, as the types for these arguments cannot be inferred. Also, as TS cannot mix explicit and inferred generic parameters, from this point on you'll have to define the `Returned` and `ThunkArg` generic parameter as well. +#### Manually Defining `thunkApi` Types + To define the types for these arguments, pass an object as the third generic argument, with type declarations for some or all of these fields: ```ts @@ -662,6 +667,23 @@ const handleUpdateUser = async (userData) => { } ``` +#### Defining a Pre-Typed `createAsyncThunk` + +As of RTK 1.9, you can define a "pre-typed" version of `createAsyncThunk` that can have the types for `state`, `dispatch`, and `extra` built in. This lets you set up those types once, so you don't have to repeat them each time you call `createAsyncThunk`. + +To do this, call `createAsyncThunk.withTypes<>()`, and pass in an object containing the field names and types for any of the fields in the `AsyncThunkConfig` type listed above. This might look like: + +```ts +const createAppAsyncThunk = createAsyncThunk.withTypes<{ + state: RootState + dispatch: AppDispatch + rejectValue: string + extra: { s: string; n: number } +}>() +``` + +Import and use that pre-typed `createAppAsyncThunk` instead of the original, and the types will be used automatically: + ## `createEntityAdapter` Typing `createEntityAdapter` only requires you to specify the entity type as the single generic argument.