diff --git a/docs/api/createListenerMiddleware.mdx b/docs/api/createListenerMiddleware.mdx index 58dd06dacb..7c626174c4 100644 --- a/docs/api/createListenerMiddleware.mdx +++ b/docs/api/createListenerMiddleware.mdx @@ -486,21 +486,16 @@ To fix this, the middleware provides types for defining "pre-typed" versions of ```ts no-transpile // listenerMiddleware.ts import { createListenerMiddleware, addListener } from '@reduxjs/toolkit' -import type { TypedStartListening, TypedAddListener } from '@reduxjs/toolkit' - import type { RootState, AppDispatch } from './store' export const listenerMiddleware = createListenerMiddleware() -export type AppStartListening = TypedStartListening - -export const startAppListening = - listenerMiddleware.startListening as AppStartListening - -export const addAppListener = addListener as TypedAddListener< +export const startAppListening = listenerMiddleware.startListening.withTypes< RootState, AppDispatch -> +>() + +export const addAppListener = addListener.withTypes() ``` Then import and use those pre-typed methods in your components. diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index 335e054cbb..df55536609 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -57,7 +57,7 @@ "@testing-library/user-event": "^13.1.5", "@types/json-stringify-safe": "^5.0.0", "@types/nanoid": "^2.1.0", - "@types/node": "^10.14.4", + "@types/node": "^20.11.0", "@types/query-string": "^6.3.0", "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", diff --git a/packages/toolkit/src/listenerMiddleware/index.ts b/packages/toolkit/src/listenerMiddleware/index.ts index 94f43e4628..88d253b4cd 100644 --- a/packages/toolkit/src/listenerMiddleware/index.ts +++ b/packages/toolkit/src/listenerMiddleware/index.ts @@ -4,27 +4,42 @@ import type { ThunkDispatch } from 'redux-thunk' import { createAction } from '../createAction' import { nanoid } from '../nanoid' +import { find } from '../utils' +import { + TaskAbortError, + listenerCancelled, + listenerCompleted, + taskCancelled, + taskCompleted, +} from './exceptions' +import { + createDelay, + createPause, + raceWithSignal, + runTask, + validateActive, +} from './task' import type { - ListenerMiddleware, - ListenerMiddlewareInstance, + AbortSignalWithReason, AddListenerOverloads, AnyListenerPredicate, CreateListenerMiddlewareOptions, - TypedAddListener, - TypedCreateListenerEntry, FallbackAddListenerOptions, + ForkOptions, + ForkedTask, + ForkedTaskExecutor, ListenerEntry, ListenerErrorHandler, - UnsubscribeListener, - TakePattern, ListenerErrorInfo, - ForkedTaskExecutor, - ForkedTask, - TypedRemoveListener, + ListenerMiddleware, + ListenerMiddlewareInstance, + TakePattern, TaskResult, - AbortSignalWithReason, + TypedAddListener, + TypedCreateListenerEntry, + TypedRemoveListener, + UnsubscribeListener, UnsubscribeListenerOptions, - ForkOptions, } from './types' import { abortControllerWithReason, @@ -32,44 +47,29 @@ import { assertFunction, catchRejection, } from './utils' -import { - listenerCancelled, - listenerCompleted, - TaskAbortError, - taskCancelled, - taskCompleted, -} from './exceptions' -import { - runTask, - validateActive, - createPause, - createDelay, - raceWithSignal, -} from './task' -import { find } from '../utils' export { TaskAbortError } from './exceptions' export type { - ListenerEffect, - ListenerMiddleware, - ListenerEffectAPI, - ListenerMiddlewareInstance, + AsyncTaskExecutor, CreateListenerMiddlewareOptions, - ListenerErrorHandler, - TypedStartListening, - TypedAddListener, - TypedStopListening, - TypedRemoveListener, - UnsubscribeListener, - UnsubscribeListenerOptions, - ForkedTaskExecutor, ForkedTask, ForkedTaskAPI, - AsyncTaskExecutor, + ForkedTaskExecutor, + ListenerEffect, + ListenerEffectAPI, + ListenerErrorHandler, + ListenerMiddleware, + ListenerMiddlewareInstance, SyncTaskExecutor, TaskCancelled, TaskRejected, TaskResolved, TaskResult, + TypedAddListener, + TypedRemoveListener, + TypedStartListening, + TypedStopListening, + UnsubscribeListener, + UnsubscribeListenerOptions, } from './types' //Overly-aggressive byte-shaving @@ -215,25 +215,27 @@ const getListenerEntryPropsFrom = (options: FallbackAddListenerOptions) => { } /** Accepts the possible options for creating a listener, and returns a formatted listener entry */ -export const createListenerEntry: TypedCreateListenerEntry = ( - options: FallbackAddListenerOptions -) => { - const { type, predicate, effect } = getListenerEntryPropsFrom(options) - - const id = nanoid() - const entry: ListenerEntry = { - id, - effect, - type, - predicate, - pending: new Set(), - unsubscribe: () => { - throw new Error('Unsubscribe not initialized') - }, - } +export const createListenerEntry: TypedCreateListenerEntry = + Object.assign( + (options: FallbackAddListenerOptions) => { + const { type, predicate, effect } = getListenerEntryPropsFrom(options) + + const id = nanoid() + const entry: ListenerEntry = { + id, + effect, + type, + predicate, + pending: new Set(), + unsubscribe: () => { + throw new Error('Unsubscribe not initialized') + }, + } - return entry -} + return entry + }, + { withTypes: () => createListenerEntry } + ) as unknown as TypedCreateListenerEntry const cancelActiveListeners = ( entry: ListenerEntry> @@ -279,9 +281,9 @@ const safelyNotifyError = ( /** * @public */ -export const addListener = createAction( - `${alm}/add` -) as TypedAddListener +export const addListener = Object.assign(createAction(`${alm}/add`), { + withTypes: () => addListener, +}) as unknown as TypedAddListener /** * @public @@ -291,9 +293,9 @@ export const clearAllListeners = createAction(`${alm}/removeAll`) /** * @public */ -export const removeListener = createAction( - `${alm}/remove` -) as TypedRemoveListener +export const removeListener = Object.assign(createAction(`${alm}/remove`), { + withTypes: () => removeListener, +}) as unknown as TypedRemoveListener const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => { console.error(`${alm}/error`, ...args) @@ -302,11 +304,17 @@ const defaultErrorHandler: ListenerErrorHandler = (...args: unknown[]) => { /** * @public */ -export function createListenerMiddleware< - S = unknown, - D extends Dispatch = ThunkDispatch, +export const createListenerMiddleware = < + StateType = unknown, + DispatchType extends Dispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + >, ExtraArgument = unknown ->(middlewareOptions: CreateListenerMiddlewareOptions = {}) { +>( + middlewareOptions: CreateListenerMiddlewareOptions = {} +) => { const listenerMap = new Map() const { extra, onError = defaultErrorHandler } = middlewareOptions @@ -324,7 +332,7 @@ export function createListenerMiddleware< } } - const startListening = (options: FallbackAddListenerOptions) => { + const startListening = ((options: FallbackAddListenerOptions) => { let entry = find( Array.from(listenerMap.values()), (existingEntry) => existingEntry.effect === options.effect @@ -335,7 +343,11 @@ export function createListenerMiddleware< } return insertEntry(entry) - } + }) as AddListenerOverloads + + Object.assign(startListening, { + withTypes: () => startListening, + }) const stopListening = ( options: FallbackAddListenerOptions & UnsubscribeListenerOptions @@ -361,15 +373,19 @@ export function createListenerMiddleware< return !!entry } + Object.assign(stopListening, { + withTypes: () => stopListening, + }) + const notifyListener = async ( entry: ListenerEntry>, action: unknown, api: MiddlewareAPI, - getOriginalState: () => S + getOriginalState: () => StateType ) => { const internalTaskController = new AbortController() const take = createTakePattern( - startListening, + startListening as AddListenerOverloads, internalTaskController.signal ) const autoJoinPromises: Promise[] = [] @@ -433,7 +449,7 @@ export function createListenerMiddleware< const clearListenerMiddleware = createClearListenerMiddleware(listenerMap) - const middleware: ListenerMiddleware = + const middleware: ListenerMiddleware = (api) => (next) => (action) => { if (!isAction(action)) { // we only want to notify listeners for action objects @@ -441,7 +457,7 @@ export function createListenerMiddleware< } if (addListener.match(action)) { - return startListening(action.payload) + return startListening(action.payload as any) } if (clearAllListeners.match(action)) { @@ -454,18 +470,18 @@ export function createListenerMiddleware< } // Need to get this state _before_ the reducer processes the action - let originalState: S | typeof INTERNAL_NIL_TOKEN = api.getState() + let originalState: StateType | typeof INTERNAL_NIL_TOKEN = api.getState() // `getOriginalState` can only be called synchronously. // @see https://github.com/reduxjs/redux-toolkit/discussions/1648#discussioncomment-1932820 - const getOriginalState = (): S => { + const getOriginalState = (): StateType => { if (originalState === INTERNAL_NIL_TOKEN) { throw new Error( `${alm}: getOriginalState can only be called synchronously` ) } - return originalState as S + return originalState as StateType } let result: unknown @@ -475,10 +491,10 @@ export function createListenerMiddleware< result = next(action) if (listenerMap.size > 0) { - let currentState = api.getState() + const currentState = api.getState() // Work around ESBuild+TS transpilation issue const listenerEntries = Array.from(listenerMap.values()) - for (let entry of listenerEntries) { + for (const entry of listenerEntries) { let runListener = false try { @@ -511,5 +527,5 @@ export function createListenerMiddleware< startListening, stopListening, clearListeners: clearListenerMiddleware, - } as ListenerMiddlewareInstance + } as ListenerMiddlewareInstance } diff --git a/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts index 78bc9dc56c..c6af789516 100644 --- a/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts +++ b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.test.ts @@ -564,7 +564,7 @@ describe('createListenerMiddleware', () => { typeof store.getState, typeof store.dispatch >, - 'effect' + 'effect' | 'withTypes' > ][] = [ ['predicate', { predicate: () => true }], @@ -1760,3 +1760,4 @@ describe('createListenerMiddleware', () => { }) }) }) + diff --git a/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.withTypes.test-d.ts b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.withTypes.test-d.ts new file mode 100644 index 0000000000..3d6a3d1775 --- /dev/null +++ b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.withTypes.test-d.ts @@ -0,0 +1,152 @@ +import type { + Action, + ThunkAction, + TypedAddListener, + TypedRemoveListener, + TypedStartListening, + TypedStopListening, +} from '@reduxjs/toolkit' +import { + addListener, + configureStore, + createAsyncThunk, + createListenerMiddleware, + createSlice, + removeListener, +} from '@reduxjs/toolkit' +import { describe, expectTypeOf, test } from 'vitest' + +export interface CounterState { + counter: number +} + +const initialState: CounterState = { + counter: 0, +} + +export const counterSlice = createSlice({ + name: 'counter', + initialState, + reducers: { + increment(state) { + state.counter++ + }, + }, +}) + +export function fetchCount(amount = 1) { + return new Promise<{ data: number }>((resolve) => + setTimeout(() => resolve({ data: amount }), 500) + ) +} + +export const incrementAsync = createAsyncThunk( + 'counter/fetchCount', + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + } +) + +const { increment } = counterSlice.actions + +const store = configureStore({ + reducer: counterSlice.reducer, +}) + +type AppStore = typeof store +type AppDispatch = typeof store.dispatch +type RootState = ReturnType +type AppThunk = ThunkAction< + ThunkReturnType, + RootState, + unknown, + Action +> + +describe('listenerMiddleware.withTypes()', () => { + const listenerMiddleware = createListenerMiddleware() + let timeout: number | undefined = undefined + let done = false + + type ExpectedTakeResultType = + | [ReturnType, RootState, RootState] + | null + + test('startListening.withTypes', () => { + const startAppListening = listenerMiddleware.startListening.withTypes< + RootState, + AppDispatch + >() + + expectTypeOf(startAppListening).toEqualTypeOf< + TypedStartListening + >() + + startAppListening({ + predicate: increment.match, + effect: async (action, listenerApi) => { + const stateBefore = listenerApi.getState() + + expectTypeOf(increment).returns.toEqualTypeOf(action) + + expectTypeOf(listenerApi.dispatch).toEqualTypeOf() + + expectTypeOf(stateBefore).toEqualTypeOf() + + let takeResult = await listenerApi.take(increment.match, timeout) + const stateCurrent = listenerApi.getState() + + expectTypeOf(takeResult).toEqualTypeOf() + + expectTypeOf(stateCurrent).toEqualTypeOf() + + timeout = 1 + takeResult = await listenerApi.take(increment.match, timeout) + + done = true + }, + }) + }) + + test('addListener.withTypes', () => { + const addAppListener = addListener.withTypes() + + expectTypeOf(addAppListener).toEqualTypeOf< + TypedAddListener + >() + + store.dispatch( + addAppListener({ + matcher: increment.match, + effect: (action, listenerApi) => { + const state = listenerApi.getState() + + expectTypeOf(state).toEqualTypeOf() + + expectTypeOf(listenerApi.dispatch).toEqualTypeOf() + }, + }) + ) + }) + + test('removeListener.withTypes', () => { + const removeAppListener = removeListener.withTypes() + + expectTypeOf(removeAppListener).toEqualTypeOf< + TypedRemoveListener + >() + }) + + test('stopListening.withTypes', () => { + const stopAppListening = listenerMiddleware.stopListening.withTypes< + RootState, + AppDispatch + >() + + expectTypeOf(stopAppListening).toEqualTypeOf< + TypedStopListening + >() + }) +}) diff --git a/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.withTypes.test.ts b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.withTypes.test.ts new file mode 100644 index 0000000000..c962aa0274 --- /dev/null +++ b/packages/toolkit/src/listenerMiddleware/tests/listenerMiddleware.withTypes.test.ts @@ -0,0 +1,116 @@ +import type { Action } from 'redux' +import type { ThunkAction } from 'redux-thunk' +import { describe, expect, test } from 'vitest' +import { configureStore } from '../../configureStore' +import { createAsyncThunk } from '../../createAsyncThunk' +import { createSlice } from '../../createSlice' +import { addListener, createListenerMiddleware, removeListener } from '../index' + +export interface CounterState { + counter: number +} + +const initialState: CounterState = { + counter: 0, +} + +export const counterSlice = createSlice({ + name: 'counter', + initialState, + reducers: { + increment(state) { + state.counter++ + }, + }, +}) + +export function fetchCount(amount = 1) { + return new Promise<{ data: number }>((resolve) => + setTimeout(() => resolve({ data: amount }), 500) + ) +} + +export const incrementAsync = createAsyncThunk( + 'counter/fetchCount', + async (amount: number) => { + const response = await fetchCount(amount) + // The value we return becomes the `fulfilled` action payload + return response.data + } +) + +const { increment } = counterSlice.actions + +const store = configureStore({ + reducer: counterSlice.reducer, +}) + +type AppStore = typeof store +type AppDispatch = typeof store.dispatch +type RootState = ReturnType +type AppThunk = ThunkAction< + ThunkReturnType, + RootState, + unknown, + Action +> + +const listenerMiddleware = createListenerMiddleware() + +const startAppListening = listenerMiddleware.startListening.withTypes< + RootState, + AppDispatch +>() + +const stopAppListening = listenerMiddleware.stopListening.withTypes< + RootState, + AppDispatch +>() + +const addAppListener = addListener.withTypes() + +const removeAppListener = removeListener.withTypes() + +describe('startAppListening.withTypes', () => { + test('should return startListening', () => { + expect(startAppListening.withTypes).toEqual(expect.any(Function)) + + expect(startAppListening.withTypes().withTypes).toEqual( + expect.any(Function) + ) + + expect(startAppListening).toBe(listenerMiddleware.startListening) + }) +}) + +describe('stopAppListening.withTypes', () => { + test('should return stopListening', () => { + expect(stopAppListening.withTypes).toEqual(expect.any(Function)) + + expect(stopAppListening.withTypes().withTypes).toEqual(expect.any(Function)) + + expect(stopAppListening).toBe(listenerMiddleware.stopListening) + }) +}) + +describe('addAppListener.withTypes', () => { + test('should return addListener', () => { + expect(addAppListener.withTypes).toEqual(expect.any(Function)) + + expect(addAppListener.withTypes().withTypes).toEqual(expect.any(Function)) + + expect(addAppListener).toBe(addListener) + }) +}) + +describe('removeAppListener.withTypes', () => { + test('should return removeListener', () => { + expect(removeAppListener.withTypes).toEqual(expect.any(Function)) + + expect(removeAppListener.withTypes().withTypes).toEqual( + expect.any(Function) + ) + + expect(removeAppListener).toBe(removeListener) + }) +}) diff --git a/packages/toolkit/src/listenerMiddleware/types.ts b/packages/toolkit/src/listenerMiddleware/types.ts index 6992275dd0..fc6a304224 100644 --- a/packages/toolkit/src/listenerMiddleware/types.ts +++ b/packages/toolkit/src/listenerMiddleware/types.ts @@ -1,14 +1,13 @@ -import type { PayloadAction, BaseActionCreator } from '../createAction' import type { - Dispatch as ReduxDispatch, - MiddlewareAPI, Middleware, + MiddlewareAPI, Action as ReduxAction, + Dispatch as ReduxDispatch, UnknownAction, } from 'redux' import type { ThunkDispatch } from 'redux-thunk' +import type { BaseActionCreator, PayloadAction } from '../createAction' import type { TaskAbortError } from './exceptions' -import { NoInfer } from '../tsHelpers' /** * @internal @@ -328,22 +327,27 @@ export type ListenerMiddleware< /** @public */ export interface ListenerMiddlewareInstance< - State = unknown, - Dispatch extends ThunkDispatch = ThunkDispatch< - State, + StateType = unknown, + DispatchType extends ThunkDispatch< + StateType, unknown, - UnknownAction - >, + ReduxAction + > = ThunkDispatch, ExtraArgument = unknown > { - middleware: ListenerMiddleware + middleware: ListenerMiddleware + startListening: AddListenerOverloads< UnsubscribeListener, - State, - Dispatch, + StateType, + DispatchType, ExtraArgument - > - stopListening: RemoveListenerOverloads + > & + TypedStartListening + + stopListening: RemoveListenerOverloads & + TypedStopListening + /** * Unsubscribes all listeners, cancels running listeners and tasks. */ @@ -401,35 +405,50 @@ export type UnsubscribeListener = ( */ export interface AddListenerOverloads< Return, - State = unknown, - Dispatch extends ReduxDispatch = ThunkDispatch, + StateType = unknown, + DispatchType extends ReduxDispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + >, ExtraArgument = unknown, AdditionalOptions = unknown > { /** Accepts a "listener predicate" that is also a TS type predicate for the action*/ - >( + < + MiddlewareActionType extends UnknownAction, + ListenerPredicateType extends ListenerPredicate< + MiddlewareActionType, + StateType + > + >( options: { actionCreator?: never type?: never matcher?: never - predicate: LP + predicate: ListenerPredicateType effect: ListenerEffect< - ListenerPredicateGuardedActionType, - State, - Dispatch, + ListenerPredicateGuardedActionType, + StateType, + DispatchType, ExtraArgument > } & AdditionalOptions ): Return /** Accepts an RTK action creator, like `incrementByAmount` */ - >( + >( options: { - actionCreator: C + actionCreator: ActionCreatorType type?: never matcher?: never predicate?: never - effect: ListenerEffect, State, Dispatch, ExtraArgument> + effect: ListenerEffect< + ReturnType, + StateType, + DispatchType, + ExtraArgument + > } & AdditionalOptions ): Return @@ -440,41 +459,60 @@ export interface AddListenerOverloads< type: T matcher?: never predicate?: never - effect: ListenerEffect, State, Dispatch, ExtraArgument> + effect: ListenerEffect< + ReduxAction, + StateType, + DispatchType, + ExtraArgument + > } & AdditionalOptions ): Return /** Accepts an RTK matcher function, such as `incrementByAmount.match` */ - >( + >( options: { actionCreator?: never type?: never - matcher: M + matcher: MatchFunctionType predicate?: never - effect: ListenerEffect, State, Dispatch, ExtraArgument> + effect: ListenerEffect< + GuardedType, + StateType, + DispatchType, + ExtraArgument + > } & AdditionalOptions ): Return /** Accepts a "listener predicate" that just returns a boolean, no type assertion */ - >( + >( options: { actionCreator?: never type?: never matcher?: never - predicate: LP - effect: ListenerEffect + predicate: ListenerPredicateType + effect: ListenerEffect< + UnknownAction, + StateType, + DispatchType, + ExtraArgument + > } & AdditionalOptions ): Return } /** @public */ export type RemoveListenerOverloads< - State = unknown, - Dispatch extends ReduxDispatch = ThunkDispatch + StateType = unknown, + DispatchType extends ReduxDispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + > > = AddListenerOverloads< boolean, - State, - Dispatch, + StateType, + DispatchType, any, UnsubscribeListenerOptions > @@ -493,61 +531,269 @@ export interface RemoveListenerAction< } /** + * A "pre-typed" version of `addListenerAction`, so the listener args are well-typed + * * @public - * A "pre-typed" version of `addListenerAction`, so the listener args are well-typed */ + */ export type TypedAddListener< - State, - Dispatch extends ReduxDispatch = ThunkDispatch, + StateType, + DispatchType extends ReduxDispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + >, ExtraArgument = unknown, - Payload = ListenerEntry, + Payload = ListenerEntry, T extends string = 'listenerMiddleware/add' > = BaseActionCreator & AddListenerOverloads< PayloadAction, - State, - Dispatch, + StateType, + DispatchType, ExtraArgument - > + > & { + /** + * Creates a "pre-typed" version of `addListener` + * where the `state` and `dispatch` types are predefined. + * + * This allows you to set the `state` and `dispatch` types once, + * eliminating the need to specify them with every `addListener` call. + * + * @returns A pre-typed `addListener` with the state and dispatch types already defined. + * + * @example + * ```ts + * import { addListener } from '@reduxjs/toolkit' + * + * export const addAppListener = addListener.withTypes() + * ``` + * + * @template OverrideStateType - The specific type of state the middleware listener operates on. + * @template OverrideDispatchType - The specific type of the dispatch function. + * + * @since 2.1.0 + */ + withTypes: < + OverrideStateType extends StateType, + OverrideDispatchType extends ReduxDispatch = ThunkDispatch< + OverrideStateType, + unknown, + UnknownAction + > + >() => TypedAddListener + } /** + * A "pre-typed" version of `removeListenerAction`, so the listener args are well-typed + * * @public - * A "pre-typed" version of `removeListenerAction`, so the listener args are well-typed */ + */ export type TypedRemoveListener< - State, - Dispatch extends ReduxDispatch = ThunkDispatch, - Payload = ListenerEntry, + StateType, + DispatchType extends ReduxDispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + >, + Payload = ListenerEntry, T extends string = 'listenerMiddleware/remove' > = BaseActionCreator & AddListenerOverloads< PayloadAction, - State, - Dispatch, + StateType, + DispatchType, any, UnsubscribeListenerOptions - > + > & { + /** + * Creates a "pre-typed" version of `removeListener` + * where the `state` and `dispatch` types are predefined. + * + * This allows you to set the `state` and `dispatch` types once, + * eliminating the need to specify them with every `removeListener` call. + * + * @returns A pre-typed `removeListener` with the state and dispatch types already defined. + * + * @example + * ```ts + * import { removeListener } from '@reduxjs/toolkit' + * + * export const removeAppListener = removeListener.withTypes() + * ``` + * + * @template OverrideStateType - The specific type of state the middleware listener operates on. + * @template OverrideDispatchType - The specific type of the dispatch function. + * + * @since 2.1.0 + */ + withTypes: < + OverrideStateType extends StateType, + OverrideDispatchType extends ReduxDispatch = ThunkDispatch< + OverrideStateType, + unknown, + UnknownAction + > + >() => TypedRemoveListener + } /** + * A "pre-typed" version of `middleware.startListening`, so the listener args are well-typed + * * @public - * A "pre-typed" version of `middleware.startListening`, so the listener args are well-typed */ + */ export type TypedStartListening< - State, - Dispatch extends ReduxDispatch = ThunkDispatch, + StateType, + DispatchType extends ReduxDispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + >, ExtraArgument = unknown -> = AddListenerOverloads +> = AddListenerOverloads< + UnsubscribeListener, + StateType, + DispatchType, + ExtraArgument +> & { + /** + * Creates a "pre-typed" version of + * {@linkcode ListenerMiddlewareInstance.startListening startListening} + * where the `state` and `dispatch` types are predefined. + * + * This allows you to set the `state` and `dispatch` types once, + * eliminating the need to specify them with every + * {@linkcode ListenerMiddlewareInstance.startListening startListening} call. + * + * @returns A pre-typed `startListening` with the state and dispatch types already defined. + * + * @example + * ```ts + * import { createListenerMiddleware } from '@reduxjs/toolkit' + * + * const listenerMiddleware = createListenerMiddleware() + * + * export const startAppListening = listenerMiddleware.startListening.withTypes< + * RootState, + * AppDispatch + * >() + * ``` + * + * @template OverrideStateType - The specific type of state the middleware listener operates on. + * @template OverrideDispatchType - The specific type of the dispatch function. + * + * @since 2.1.0 + */ + withTypes: < + OverrideStateType extends StateType, + OverrideDispatchType extends ReduxDispatch = ThunkDispatch< + OverrideStateType, + unknown, + UnknownAction + > + >() => TypedStartListening +} -/** @public - * A "pre-typed" version of `middleware.stopListening`, so the listener args are well-typed */ +/** + * A "pre-typed" version of `middleware.stopListening`, so the listener args are well-typed + * + * @public + */ export type TypedStopListening< - State, - Dispatch extends ReduxDispatch = ThunkDispatch -> = RemoveListenerOverloads + StateType, + DispatchType extends ReduxDispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + > +> = RemoveListenerOverloads & { + /** + * Creates a "pre-typed" version of + * {@linkcode ListenerMiddlewareInstance.stopListening stopListening} + * where the `state` and `dispatch` types are predefined. + * + * This allows you to set the `state` and `dispatch` types once, + * eliminating the need to specify them with every + * {@linkcode ListenerMiddlewareInstance.stopListening stopListening} call. + * + * @returns A pre-typed `stopListening` with the state and dispatch types already defined. + * + * @example + * ```ts + * import { createListenerMiddleware } from '@reduxjs/toolkit' + * + * const listenerMiddleware = createListenerMiddleware() + * + * export const stopAppListening = listenerMiddleware.stopListening.withTypes< + * RootState, + * AppDispatch + * >() + * ``` + * + * @template OverrideStateType - The specific type of state the middleware listener operates on. + * @template OverrideDispatchType - The specific type of the dispatch function. + * + * @since 2.1.0 + */ + withTypes: < + OverrideStateType extends StateType, + OverrideDispatchType extends ReduxDispatch = ThunkDispatch< + OverrideStateType, + unknown, + UnknownAction + > + >() => TypedStopListening +} -/** @public - * A "pre-typed" version of `createListenerEntry`, so the listener args are well-typed */ +/** + * A "pre-typed" version of `createListenerEntry`, so the listener args are well-typed + * + * @public + */ export type TypedCreateListenerEntry< - State, - Dispatch extends ReduxDispatch = ThunkDispatch -> = AddListenerOverloads, State, Dispatch> + StateType, + DispatchType extends ReduxDispatch = ThunkDispatch< + StateType, + unknown, + UnknownAction + > +> = AddListenerOverloads< + ListenerEntry, + StateType, + DispatchType +> & { + /** + * Creates a "pre-typed" version of `createListenerEntry` + * where the `state` and `dispatch` types are predefined. + * + * This allows you to set the `state` and `dispatch` types once, eliminating + * the need to specify them with every `createListenerEntry` call. + * + * @returns A pre-typed `createListenerEntry` with the state and dispatch types already defined. + * + * @example + * ```ts + * import { createListenerEntry } from '@reduxjs/toolkit' + * + * export const createAppListenerEntry = createListenerEntry.withTypes< + * RootState, + * AppDispatch + * >() + * ``` + * + * @template OverrideStateType - The specific type of state the middleware listener operates on. + * @template OverrideDispatchType - The specific type of the dispatch function. + * + * @since 2.1.0 + */ + withTypes: < + OverrideStateType extends StateType, + OverrideDispatchType extends ReduxDispatch = ThunkDispatch< + OverrideStateType, + unknown, + UnknownAction + > + >() => TypedStopListening +} /** * Internal Types diff --git a/packages/toolkit/src/tests/tsconfig.typetests.json b/packages/toolkit/src/tests/tsconfig.typetests.json index f63fa81c64..5fe2280ed9 100644 --- a/packages/toolkit/src/tests/tsconfig.typetests.json +++ b/packages/toolkit/src/tests/tsconfig.typetests.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.test.json", "compilerOptions": { - "skipLibCheck": true - } + "skipLibCheck": true, + "rootDir": "../../src" + }, + "include": ["../dynamicMiddleware", "../entities", "../listenerMiddleware"] } diff --git a/packages/toolkit/vitest.config.ts b/packages/toolkit/vitest.config.ts index c65f978f92..45b67cf6bd 100644 --- a/packages/toolkit/vitest.config.ts +++ b/packages/toolkit/vitest.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'vitest/config' -import path from 'path' -import { fileURLToPath } from 'url' +import path from 'node:path' +import { fileURLToPath } from 'node:url' // No __dirname under Node ESM const __filename = fileURLToPath(import.meta.url) @@ -9,11 +9,13 @@ const __dirname = path.dirname(__filename) export default defineConfig({ test: { + typecheck: { only: true, tsconfig: './src/tests/tsconfig.typetests.json' }, globals: true, environment: 'jsdom', setupFiles: ['./vitest.setup.js'], include: ['./src/**/*.(spec|test).[jt]s?(x)'], alias: { + // prettier-ignore '@reduxjs/toolkit/query/react': path.join(__dirname,'./src/query/react/index.ts'), // @remap-prod-remove-line '@reduxjs/toolkit/query': path.join(__dirname, './src/query/index.ts'), // @remap-prod-remove-line '@reduxjs/toolkit/react': path.join(__dirname, './src/index.ts'), // @remap-prod-remove-line diff --git a/yarn.lock b/yarn.lock index 3ea896b4e8..91870e084e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7025,7 +7025,7 @@ __metadata: "@testing-library/user-event": ^13.1.5 "@types/json-stringify-safe": ^5.0.0 "@types/nanoid": ^2.1.0 - "@types/node": ^10.14.4 + "@types/node": ^20.11.0 "@types/query-string": ^6.3.0 "@types/react": ^18.0.12 "@types/react-dom": ^18.0.5 @@ -8481,13 +8481,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^10.14.4": - version: 10.17.60 - resolution: "@types/node@npm:10.17.60" - checksum: 2cdb3a77d071ba8513e5e8306fa64bf50e3c3302390feeaeff1fd325dd25c8441369715dfc8e3701011a72fed5958c7dfa94eb9239a81b3c286caa4d97db6eef - languageName: node - linkType: hard - "@types/node@npm:^12.0.0": version: 12.20.37 resolution: "@types/node@npm:12.20.37" @@ -8502,6 +8495,15 @@ __metadata: languageName: node linkType: hard +"@types/node@npm:^20.11.0": + version: 20.11.0 + resolution: "@types/node@npm:20.11.0" + dependencies: + undici-types: ~5.26.4 + checksum: 1bd6890db7e0404d11c33d28f46f19f73256f0ba35d19f0ef2a0faba09f366f188915fb9338eebebcc472075c1c4941e17c7002786aa69afa44980737846b200 + languageName: node + linkType: hard + "@types/normalize-package-data@npm:^2.4.0": version: 2.4.0 resolution: "@types/normalize-package-data@npm:2.4.0" @@ -29342,6 +29344,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"undici-types@npm:~5.26.4": + version: 5.26.5 + resolution: "undici-types@npm:5.26.5" + checksum: 3192ef6f3fd5df652f2dc1cd782b49d6ff14dc98e5dced492aa8a8c65425227da5da6aafe22523c67f035a272c599bb89cfe803c1db6311e44bed3042fc25487 + languageName: node + linkType: hard + "unherit@npm:^1.0.4": version: 1.1.3 resolution: "unherit@npm:1.1.3"