diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 4b4ad04..5495f01 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -1,4 +1,6 @@ { "sandboxes": ["vanilla", "vanilla-ts"], - "node": "14" + "node": "14", + "buildCommand": "build", + "packages": ["."] } diff --git a/src/index.ts b/src/index.ts index 1ac48b1..a550786 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,36 +9,49 @@ export type { ThunkMiddleware } from './types' +/** A function that accepts a potential "extra argument" value to be injected later, + * and returns an instance of the thunk middleware that uses that value + */ function createThunkMiddleware< - TState = any, - TBasicAction extends Action = AnyAction, - TExtraThunkArg = undefined ->(extraArgument?: TExtraThunkArg) { - const middleware: ThunkMiddleware = + State = any, + BasicAction extends Action = AnyAction, + ExtraThunkArg = undefined +>(extraArgument?: ExtraThunkArg) { + // Standard Redux middleware definition pattern: + // See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware + const middleware: ThunkMiddleware = ({ dispatch, getState }) => next => action => { + // The thunk middleware looks for any functions that were passed to `store.dispatch`. + // If this "action" is really a function, call it and return the result. if (typeof action === 'function') { + // Inject the store's `dispatch` and `getState` methods, as well as any "extra arg" return action(dispatch, getState, extraArgument) } + // Otherwise, pass the action down the middleware chain as usual return next(action) } return middleware } +/** The standard thunk middleware, with no extra argument included */ const thunk = createThunkMiddleware() +// Attach the factory function so users can create a customized version +// with whatever "extra arg" they want to inject into their thunks // eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-expect-error +// @ts-ignore thunk.withExtraArgument = createThunkMiddleware +// Convince TS that the default export has the right type for `withExtraArgument` export default thunk as typeof thunk & ThunkMiddleware & { withExtraArgument< - TExtraThunkArg, - TState = any, - TBasicAction extends Action = AnyAction + ExtraThunkArg, + State = any, + BasicAction extends Action = AnyAction >( - extraArgument: TExtraThunkArg - ): ThunkMiddleware + extraArgument: ExtraThunkArg + ): ThunkMiddleware } diff --git a/src/types.ts b/src/types.ts index c0b4f17..3bd3368 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Action, AnyAction, Middleware } from 'redux' +import { Action, AnyAction, Middleware, Dispatch } from 'redux' /** * The dispatch method as modified by React-Thunk; overloaded so that you can @@ -6,26 +6,30 @@ import { Action, AnyAction, Middleware } from 'redux' * - standard (object) actions: `dispatch()` returns the action itself * - thunk actions: `dispatch()` returns the thunk's return value * - * @template TState The redux state - * @template TExtraThunkArg The extra argument passed to the inner function of + * @template State The redux state + * @template ExtraThunkArg The extra argument passed to the inner function of * thunks (if specified when setting up the Thunk middleware) - * @template TBasicAction The (non-thunk) actions that can be dispatched. + * @template BasicAction The (non-thunk) actions that can be dispatched. */ -export interface ThunkDispatch< - TState, - TExtraThunkArg, - TBasicAction extends Action -> { - ( - thunkAction: ThunkAction - ): TReturnType - (action: A): A - // This overload is the union of the two above (see TS issue #14107). - ( - action: - | TAction - | ThunkAction - ): TAction | TReturnType +export interface ThunkDispatch + extends Dispatch { + // When the thunk middleware is added, `store.dispatch` now has three overloads: + + // 1) The base overload, which accepts a standard action object, and returns that action object + + // 2) The specific thunk function overload + /** Accepts a thunk function, runs it, and returns whatever the thunk itself returns */ + ( + thunkAction: ThunkAction + ): ReturnType + + // 3) + /** A union of the other two overloads. This overload exists to work around a problem + * with TS inference ( see https://github.com/microsoft/TypeScript/issues/14107 ) + */ + ( + action: Action | ThunkAction + ): Action | ReturnType } /** @@ -35,22 +39,22 @@ export interface ThunkDispatch< * Also known as the "thunk inner function", when used with the typical pattern * of an action creator function that returns a thunk action. * - * @template TReturnType The return type of the thunk's inner function - * @template TState The redux state - * @template TExtraThunkARg Optional extra argument passed to the inner function + * @template ReturnType The return type of the thunk's inner function + * @template State The redux state + * @template ExtraThunkArg Optional extra argument passed to the inner function * (if specified when setting up the Thunk middleware) - * @template TBasicAction The (non-thunk) actions that can be dispatched. + * @template BasicAction The (non-thunk) actions that can be dispatched. */ export type ThunkAction< - TReturnType, - TState, - TExtraThunkArg, - TBasicAction extends Action + ReturnType, + State, + ExtraThunkArg, + BasicAction extends Action > = ( - dispatch: ThunkDispatch, - getState: () => TState, - extraArgument: TExtraThunkArg -) => TReturnType + dispatch: ThunkDispatch, + getState: () => State, + extraArgument: ExtraThunkArg +) => ReturnType /** * A generic type that takes a thunk action creator and returns a function @@ -58,26 +62,26 @@ export type ThunkAction< * bindActionCreators(): a function that takes the arguments of the outer * function, and returns the return type of the inner "thunk" function. * - * @template TActionCreator Thunk action creator to be wrapped + * @template ActionCreator Thunk action creator to be wrapped */ export type ThunkActionDispatch< - TActionCreator extends (...args: any[]) => ThunkAction + ActionCreator extends (...args: any[]) => ThunkAction > = ( - ...args: Parameters -) => ReturnType> + ...args: Parameters +) => ReturnType> /** - * @template TState The redux state - * @template TBasicAction The (non-thunk) actions that can be dispatched - * @template TExtraThunkArg An optional extra argument to pass to a thunk's + * @template State The redux state + * @template BasicAction The (non-thunk) actions that can be dispatched + * @template ExtraThunkArg An optional extra argument to pass to a thunk's * inner function. (Only used if you call `thunk.withExtraArgument()`) */ export type ThunkMiddleware< - TState = any, - TBasicAction extends Action = AnyAction, - TExtraThunkArg = undefined + State = any, + BasicAction extends Action = AnyAction, + ExtraThunkArg = undefined > = Middleware< - ThunkDispatch, - TState, - ThunkDispatch + ThunkDispatch, + State, + ThunkDispatch > diff --git a/typescript_test/typescript.ts b/typescript_test/typescript.ts index 627e856..1ea9c0f 100644 --- a/typescript_test/typescript.ts +++ b/typescript_test/typescript.ts @@ -1,5 +1,11 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { applyMiddleware, bindActionCreators, createStore } from 'redux' +import { + applyMiddleware, + bindActionCreators, + createStore, + Action, + AnyAction +} from 'redux' import thunk, { ThunkAction, @@ -71,6 +77,7 @@ const storeThunkArg = createStore( thunk.withExtraArgument('bar') as ThunkMiddleware ) ) +storeThunkArg.dispatch({ type: 'FOO' }) storeThunkArg.dispatch((dispatch, getState, extraArg) => { const bar: string = extraArg @@ -145,3 +152,15 @@ const untypedStore = createStore(fakeReducer, applyMiddleware(thunk)) untypedStore.dispatch(anotherThunkAction()) // @ts-expect-error untypedStore.dispatch(promiseThunkAction()).then(() => Promise.resolve()) + +// #248: Need a union overload to handle generic dispatched types +function testIssue248() { + const dispatch: ThunkDispatch = undefined as any + + function dispatchWrap( + action: Action | ThunkAction + ) { + // Should not have an error here thanks to the extra union overload + dispatch(action) + } +} diff --git a/webpack.config.babel.js b/webpack.config.babel.js deleted file mode 100644 index 3de7d2d..0000000 --- a/webpack.config.babel.js +++ /dev/null @@ -1,39 +0,0 @@ -import webpack from 'webpack'; -import path from 'path'; - -const { NODE_ENV } = process.env; - -const plugins = [ - new webpack.DefinePlugin({ - 'process.env.NODE_ENV': JSON.stringify(NODE_ENV), - }), -]; - -const filename = `redux-thunk${NODE_ENV === 'production' ? '.min' : ''}.js`; - -export default { - mode: NODE_ENV === 'production' ? 'production' : 'development', - - module: { - rules: [ - { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, - ], - }, - - entry: [ - './src/index', - ], - - optimization: { - minimize: NODE_ENV === 'production', - }, - - output: { - path: path.join(__dirname, 'dist'), - filename, - library: 'ReduxThunk', - libraryTarget: 'umd', - }, - - plugins, -};