diff --git a/.eslintrc b/.eslintrc index 6dad93f..5df1a73 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,6 +19,8 @@ "rules": { "padded-blocks": 0, "no-use-before-define": [2, "nofunc"], - "no-unused-expressions": 0 + "no-unused-expressions": 0, + "import/no-unresolved": 0, + "import/named": 0 } } diff --git a/extend-redux.d.ts b/extend-redux.d.ts new file mode 100644 index 0000000..57cf263 --- /dev/null +++ b/extend-redux.d.ts @@ -0,0 +1,42 @@ +import { ThunkAction } from './src/index'; + +/** + * Globally alter the Redux `bindActionCreators` and `Dispatch` types to assume + * that the thunk middleware always exists, for ease of use. + * This is kept as a separate file that may be optionally imported, to + * avoid polluting the default types in case the thunk middleware is _not_ + * actually being used. + * + * To add these types to your app: + * import 'redux-thunk/extend-redux' + */ +declare module 'redux' { + /** + * Overload for bindActionCreators redux function, returns expects responses + * from thunk actions + */ + function bindActionCreators< + TActionCreators extends ActionCreatorsMapObject + >( + actionCreators: TActionCreators, + dispatch: Dispatch, + ): { + [TActionCreatorName in keyof TActionCreators]: ReturnType< + TActionCreators[TActionCreatorName] + > extends ThunkAction + ? ( + ...args: Parameters + ) => ReturnType> + : TActionCreators[TActionCreatorName]; + }; + + /* + * Overload to add thunk support to Redux's dispatch() function. + * Useful for react-redux or any other library which could use this type. + */ + export interface Dispatch { + ( + thunkAction: ThunkAction, + ): TReturnType; + } +} diff --git a/package.json b/package.json index db70e98..e6e1f09 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,17 @@ "lib", "es", "src", - "dist" + "dist", + "extend-redux.d.ts" ], "scripts": { "clean": "rimraf lib dist es", "prepublishOnly": "npm run clean && npm run lint && npm run test && npm run build", "lint": "eslint src test", "test": "cross-env BABEL_ENV=commonjs mocha --require @babel/register --reporter spec test/*.js", - "test:typescript": "tsc --noEmit -p test/tsconfig.json", + "test:typescript": "npm run test:typescript:main && npm run test:typescript:extended", + "test:typescript:main": "tsc --noEmit -p test/tsconfig.json", + "test:typescript:extended": "tsc --noEmit -p test/typescript_extended/tsconfig.json", "build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es", "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", "build:es": "cross-env BABEL_ENV=es babel src --out-dir es", diff --git a/src/index.d.ts b/src/index.d.ts index 146e3f6..6f09ea6 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -28,7 +28,9 @@ export interface ThunkDispatch< (action: A): A; // This overload is the union of the two above (see TS issue #14107). ( - action: TAction | ThunkAction, + action: + | TAction + | ThunkAction, ): TAction | TReturnType; } @@ -97,37 +99,3 @@ declare const thunk: ThunkMiddleware & { }; export default thunk; - -/** - * Redux behaviour changed by middleware, so overloads here - */ -declare module 'redux' { - /** - * Overload for bindActionCreators redux function, returns expects responses - * from thunk actions - */ - function bindActionCreators< - TActionCreators extends ActionCreatorsMapObject - >( - actionCreators: TActionCreators, - dispatch: Dispatch, - ): { - [TActionCreatorName in keyof TActionCreators]: ReturnType< - TActionCreators[TActionCreatorName] - > extends ThunkAction - ? ( - ...args: Parameters - ) => ReturnType> - : TActionCreators[TActionCreatorName]; - }; - - /* - * Overload to add thunk support to Redux's dispatch() function. - * Useful for react-redux or any other library which could use this type. - */ - export interface Dispatch { - ( - thunkAction: ThunkAction, - ): TReturnType; - } -} diff --git a/test/tsconfig.json b/test/tsconfig.json index 0152ab3..8b8e40a 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -1,18 +1,8 @@ { "compilerOptions": { - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", - "emitDeclarationOnly": false, + "module": "commonjs", "strict": true, - "noEmit": true, - "target": "esnext", - "jsx": "react", - "baseUrl": ".", - "skipLibCheck": true, - "noImplicitReturns": false, - "noUnusedLocals": false + "target": "ES2015" }, - "include": ["**/*.ts"] + "include": ["typescript.ts"] } diff --git a/test/typescript.ts b/test/typescript.ts index 1a0cc42..98f3f0e 100644 --- a/test/typescript.ts +++ b/test/typescript.ts @@ -12,23 +12,26 @@ import thunk, { ThunkMiddleware, } from '../src/index'; -type State = { +export type State = { foo: string; }; -type Actions = { type: 'FOO' } | { type: 'BAR'; result: number }; +export type Actions = { type: 'FOO' } | { type: 'BAR'; result: number }; -type ThunkResult = ThunkAction; +export type ThunkResult = ThunkAction; -const initialState: State = { +export const initialState: State = { foo: 'foo', }; -function fakeReducer(state: State = initialState, action: Actions): State { +export function fakeReducer( + state: State = initialState, + action: Actions, +): State { return state; } -const store = createStore( +export const store = createStore( fakeReducer, applyMiddleware(thunk as ThunkMiddleware), ); @@ -57,49 +60,13 @@ function testGetState(): ThunkResult { }; } -function anotherThunkAction(): ThunkResult { +export function anotherThunkAction(): ThunkResult { return (dispatch, getState) => { dispatch({ type: 'FOO' }); return 'hello'; }; } -function promiseThunkAction(): ThunkResult> { - return async (dispatch, getState) => { - dispatch({ type: 'FOO' }); - return false; - }; -} - -const standardAction = () => ({ type: 'FOO' }); - -interface ActionDispatchs { - anotherThunkAction: ThunkActionDispatch; - promiseThunkAction: ThunkActionDispatch; - standardAction: typeof standardAction; -} - -// test that bindActionCreators correctly returns actions responses of ThunkActions -// also ensure standard action creators still work as expected -const actions: ActionDispatchs = bindActionCreators( - { - anotherThunkAction, - promiseThunkAction, - standardAction, - }, - store.dispatch, -); - -actions.anotherThunkAction() === 'hello'; -// @ts-expect-error -actions.anotherThunkAction() === false; -actions.promiseThunkAction().then((res) => console.log(res)); -// @ts-expect-error -actions.promiseThunkAction().prop; -actions.standardAction().type; -// @ts-expect-error -actions.standardAction().other; - store.dispatch({ type: 'FOO' }); // @ts-expect-error store.dispatch({ type: 'BAR' }); @@ -150,7 +117,45 @@ const callDispatchAny = ( .then(() => console.log('done')); }; +function promiseThunkAction(): ThunkResult> { + return async (dispatch, getState) => { + dispatch({ type: 'FOO' }); + return false; + }; +} + +const standardAction = () => ({ type: 'FOO' }); + +interface ActionDispatchs { + anotherThunkAction: ThunkActionDispatch; + promiseThunkAction: ThunkActionDispatch; + standardAction: typeof standardAction; +} + +// Without a global module overload, this should fail +// @ts-expect-error +const actions: ActionDispatchs = bindActionCreators( + { + anotherThunkAction, + promiseThunkAction, + standardAction, + }, + store.dispatch, +); + +actions.anotherThunkAction() === 'hello'; +// @ts-expect-error +actions.anotherThunkAction() === false; +actions.promiseThunkAction().then((res) => console.log(res)); +// @ts-expect-error +actions.promiseThunkAction().prop; +actions.standardAction().type; +// @ts-expect-error +actions.standardAction().other; + const untypedStore = createStore(fakeReducer, applyMiddleware(thunk)); +// @ts-expect-error untypedStore.dispatch(anotherThunkAction()); +// @ts-expect-error untypedStore.dispatch(promiseThunkAction()).then(() => Promise.resolve()); diff --git a/test/typescript_extended/extended-redux.ts b/test/typescript_extended/extended-redux.ts new file mode 100644 index 0000000..d72ffc0 --- /dev/null +++ b/test/typescript_extended/extended-redux.ts @@ -0,0 +1,87 @@ +// This file tests behavior of types when we import the additional "extend-redux" module, +// which globally alters the Redux `bindActionCreators` and `Dispatch` types to assume +// that the thunk middleware always exists + +import { + applyMiddleware, + bindActionCreators, + createStore, + Dispatch, +} from 'redux'; + +import thunk, { + ThunkAction, + ThunkActionDispatch, + ThunkDispatch, + ThunkMiddleware, +} from '../../src/index'; + +// MAGIC: Import a TS file that extends the `redux` module types +// This file must be kept separate from the primary typetest file to keep from +// polluting the type definitions over there +import '../../extend-redux'; + +export type State = { + foo: string; +}; + +export type Actions = { type: 'FOO' } | { type: 'BAR'; result: number }; + +export type ThunkResult = ThunkAction; + +export const initialState: State = { + foo: 'foo', +}; + +export function fakeReducer( + state: State = initialState, + action: Actions, +): State { + return state; +} + +export const store = createStore( + fakeReducer, + applyMiddleware(thunk as ThunkMiddleware), +); + +export function anotherThunkAction(): ThunkResult { + return (dispatch, getState) => { + dispatch({ type: 'FOO' }); + return 'hello'; + }; +} + +function promiseThunkAction(): ThunkResult> { + return async (dispatch, getState) => { + dispatch({ type: 'FOO' }); + return false; + }; +} + +const standardAction = () => ({ type: 'FOO' }); + +interface ActionDispatchs { + anotherThunkAction: ThunkActionDispatch; + promiseThunkAction: ThunkActionDispatch; + standardAction: typeof standardAction; +} + +// test that bindActionCreators correctly returns actions responses of ThunkActions +// also ensure standard action creators still work as expected. +// Unlike the main file, this declaration should compile okay because we've imported +// the global module override +const actions: ActionDispatchs = bindActionCreators( + { + anotherThunkAction, + promiseThunkAction, + standardAction, + }, + store.dispatch, +); + +const untypedStore = createStore(fakeReducer, applyMiddleware(thunk)); + +// Similarly, both of these declarations should pass okay as well +untypedStore.dispatch(anotherThunkAction()); +untypedStore.dispatch(promiseThunkAction()).then(() => Promise.resolve()); diff --git a/test/typescript_extended/tsconfig.json b/test/typescript_extended/tsconfig.json new file mode 100644 index 0000000..763ae2d --- /dev/null +++ b/test/typescript_extended/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "strict": true, + "target": "ES2015" + }, + "include": ["extended-redux.ts"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..cef87bf --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node", + "esModuleInterop": true, + "skipLibCheck": true, + "allowJs": true, + "jsx": "react", + "declaration": true, + "emitDeclarationOnly": true, + "outDir": "./es", + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "rootDirs": ["./src", "./test"], + "rootDir": "./src" + }, + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist"] +}