diff --git a/etc/redux-toolkit.api.md b/etc/redux-toolkit.api.md index 6a7abc59f4..fc09190c24 100644 --- a/etc/redux-toolkit.api.md +++ b/etc/redux-toolkit.api.md @@ -6,20 +6,31 @@ import { Action } from 'redux'; import { ActionCreator } from 'redux'; +import { AdvancedComponentDecorator } from 'react-redux'; import { AnyAction } from 'redux'; +import { ConnectOptions } from 'react-redux'; import { default as createNextState } from 'immer'; import { createSelector } from 'reselect'; import { current } from 'immer'; import { DeepPartial } from 'redux'; import { Dispatch } from 'redux'; import { Draft } from 'immer'; +import { InferableComponentEnhancer } from 'react-redux'; +import { InferableComponentEnhancerWithProps } from 'react-redux'; +import { MapDispatchToPropsNonObject } from 'react-redux'; +import { MapDispatchToPropsParam } from 'react-redux'; +import { MapStateToPropsParam } from 'react-redux'; +import { MergeProps } from 'react-redux'; import { Middleware } from 'redux'; +import { Options } from 'react-redux'; import { OutputParametricSelector } from 'reselect'; import { OutputSelector } from 'reselect'; import { ParametricSelector } from 'reselect'; import { Reducer } from 'redux'; import { ReducersMapObject } from 'redux'; +import { ResolveThunks } from 'react-redux'; import { Selector } from 'reselect'; +import { SelectorFactory } from 'react-redux'; import { Store } from 'redux'; import { StoreEnhancer } from 'redux'; import { ThunkAction } from 'redux-thunk'; @@ -170,8 +181,16 @@ export interface CreateSliceOptions; } +// @beta (undocumented) +export const createThunk: >(thunkActionCreator: ThunkActionCreator) => ThunkActionCreator; + export { current } +// @beta (undocumented) +export function curryForStoreType(): CurryType; + // @public (undocumented) export interface Dictionary extends DictionaryNum { // (undocumented) @@ -182,6 +201,12 @@ export { Draft } // @public export interface EnhancedStore = Middlewares> extends Store { + // (undocumented) + [storeDescriptionKey]: { + RootState: S; + Dispatch: DispatchForMiddlewares & Dispatch; + ThunkExtraArgument: ExtraFromMiddlewares; + }; dispatch: DispatchForMiddlewares & Dispatch; } diff --git a/package-lock.json b/package-lock.json index a455a02c6f..79ea7c4109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1278,6 +1278,16 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", + "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "dev": true, + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1357,6 +1367,34 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/prop-types": { + "version": "15.7.3", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", + "dev": true + }, + "@types/react": { + "version": "16.9.45", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.45.tgz", + "integrity": "sha512-vv950slTF5UZ5eDOf13b8qC1SD4rTvkqg3HfaUKzr17U97oeJZAa+dUaIHn0QoOJflNTIt6Pem9MmapULs9dkA==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-redux": { + "version": "7.1.9", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz", + "integrity": "sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==", + "dev": true, + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, "@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", @@ -2541,6 +2579,12 @@ "cssom": "0.3.x" } }, + "csstype": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.2.tgz", + "integrity": "sha512-ofovWglpqoqbfLNOTBNZLSbMuGrblAf1efvvArGKOZMBrIoJeu5UsAipQolkijtyQx5MtAzT/J9IHj/CEY1mJw==", + "dev": true + }, "damerau-levenshtein": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.5.tgz", @@ -4515,6 +4559,15 @@ } } }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "requires": { + "react-is": "^16.7.0" + } + }, "hosted-git-info": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", diff --git a/package.json b/package.json index 7dd7ffe133..19df541a09 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "@types/json-stringify-safe": "^5.0.0", "@types/nanoid": "^2.1.0", "@types/node": "^10.14.4", + "@types/react-redux": "^7.1.9", "axios": "^0.19.2", "console-testing-library": "^0.3.1", "eslint-config-react-app": "^5.0.1", diff --git a/src/configureStore.ts b/src/configureStore.ts index b978716ce4..ff63d6aa95 100644 --- a/src/configureStore.ts +++ b/src/configureStore.ts @@ -17,14 +17,14 @@ import { composeWithDevTools, EnhancerOptions as DevToolsOptions } from './devtoolsExtension' - +import { storeDescriptionKey } from './curriedTypes' import isPlainObject from './isPlainObject' import { ThunkMiddlewareFor, curryGetDefaultMiddleware, CurriedGetDefaultMiddleware } from './getDefaultMiddleware' -import { DispatchForMiddlewares } from './tsHelpers' +import { DispatchForMiddlewares, ExtraFromMiddlewares } from './tsHelpers' const IS_PRODUCTION = process.env.NODE_ENV === 'production' @@ -110,6 +110,12 @@ export interface EnhancedStore< * @inheritdoc */ dispatch: DispatchForMiddlewares & Dispatch + + [storeDescriptionKey]: { + RootState: S + Dispatch: DispatchForMiddlewares & Dispatch + ThunkExtraArgument: ExtraFromMiddlewares + } } /** 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/createThunk.ts b/src/createThunk.ts new file mode 100644 index 0000000000..f023d70d65 --- /dev/null +++ b/src/createThunk.ts @@ -0,0 +1,25 @@ +import { ThunkDispatch } from 'redux-thunk' +import { AnyAction } from 'redux' + +export type ThunkActionCreator< + Args extends any[], + R, + State, + Extra, + Dispatch +> = ( + ...args: Args +) => (dispatch: Dispatch, getState: () => State, extra: Extra) => R + +/** + * @beta + */ +export const createThunk = < + Args extends any[], + R, + State = unknown, + Extra = unknown, + Dispatch = ThunkDispatch +>( + thunkActionCreator: ThunkActionCreator +) => thunkActionCreator diff --git a/src/curriedTypes/RTK.ts b/src/curriedTypes/RTK.ts new file mode 100644 index 0000000000..18e0dd4001 --- /dev/null +++ b/src/curriedTypes/RTK.ts @@ -0,0 +1,53 @@ +export interface CurryableTypes { + createAsyncThunk: typeof createAsyncThunk + createThunk: typeof createThunk +} + +export interface CurriedType { + createAsyncThunk: CurriedCreateAsyncThunk< + Args['RootState'], + Args['Dispatch'], + Args['ThunkExtraArgument'] + > + createThunk: CurriedCreateThunk< + Args['RootState'], + Args['Dispatch'], + Args['ThunkExtraArgument'] + > +} + +/* eslint-disable import/first */ +import { StoreDescription } from './' +import { + createAsyncThunk, + AsyncThunk, + AsyncThunkPayloadCreator, + AsyncThunkConfig, + AsyncThunkOptions +} from '../createAsyncThunk' +import { createThunk, ThunkActionCreator } from '../createThunk' + +type CurriedCreateAsyncThunk = < + Returned, + ThunkArg = void, + ThunkApiConfig extends Omit< + AsyncThunkConfig, + 'dispatch' | 'state' | 'extra' + > = {} +>( + typePrefix: string, + payloadCreator: AsyncThunkPayloadCreator< + Returned, + ThunkArg, + ThunkApiConfig & { + dispatch: Dispatch + state: RootState + extra: Extra + } + >, + options?: AsyncThunkOptions +) => AsyncThunk + +type CurriedCreateThunk = ( + thunkActionCreator: ThunkActionCreator +) => ThunkActionCreator diff --git a/src/curriedTypes/index.ts b/src/curriedTypes/index.ts new file mode 100644 index 0000000000..44fcf1961b --- /dev/null +++ b/src/curriedTypes/index.ts @@ -0,0 +1,50 @@ +import { + CurriedType as RRCurriedType, + CurryableTypes as RRCurryableType +} from './react-redux' +import { + CurriedType as RTKCurriedType, + CurryableTypes as RTKCurryableType +} from './RTK' + +import { UnionToIntersection } from '../tsHelpers' + +export declare const storeDescriptionKey: unique symbol +/** + * @beta + */ +export function curryForStoreType< + Store extends { [storeDescriptionKey]: StoreDescription } +>(): CurryType { + return (curry: any) => curry +} + +export interface StoreDescription { + Dispatch: any + RootState: any + ThunkExtraArgument: any +} + +type CurrySingleType = UnionToIntersection< + { + [K in keyof CurryableTypes]: ( + curry: CurryableTypes[K] + ) => CurriedType[K] + }[keyof CurryableTypes] +> + +type CurryMultipleTypes = { + >(obj: Obj): Pick< + CurriedType, + keyof Obj & keyof CurriedType + > +} + +export type CurryType = CurrySingleType & + CurryMultipleTypes + +export interface CurryableTypes extends RRCurryableType, RTKCurryableType {} + +export interface CurriedType + extends RRCurriedType, + RTKCurriedType {} diff --git a/src/curriedTypes/react-redux.ts b/src/curriedTypes/react-redux.ts new file mode 100644 index 0000000000..89f508e290 --- /dev/null +++ b/src/curriedTypes/react-redux.ts @@ -0,0 +1,210 @@ +export interface CurryableTypes { + useDispatch: typeof import('react-redux').useDispatch + useSelector: typeof import('react-redux').useSelector + connect: typeof import('react-redux').connect + connectAdvanced: typeof import('react-redux').connectAdvanced +} + +export interface CurriedType { + useDispatch: UseDispatch + useSelector: UseSelector + connect: Connect + connectAdvanced: ConnectAdvanced +} + +/* eslint-disable import/first */ +import { + InferableComponentEnhancer, + MapStateToPropsParam, + InferableComponentEnhancerWithProps, + MapDispatchToPropsNonObject, + MapDispatchToPropsParam, + ResolveThunks, + MergeProps, + Options, + SelectorFactory, + ConnectOptions, + AdvancedComponentDecorator +} from 'react-redux' + +type UseDispatch = { + (): Dispatch +} + +type UseSelector = { + ( + selector: (state: RootState) => TSelected, + equalityFn?: (left: TSelected, right: TSelected) => boolean + ): TSelected +} + +interface TypedDispatchProp { + dispatch: Dispatch +} + +// copied straight from the RR types, uncommented `State` generic +interface Connect { + (): InferableComponentEnhancer> + + < + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {} + // State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam + ): InferableComponentEnhancerWithProps< + TStateProps & TypedDispatchProp, + TOwnProps + > + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject + ): InferableComponentEnhancerWithProps + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam + ): InferableComponentEnhancerWithProps< + ResolveThunks, + TOwnProps + > + + < + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {} + //State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject + ): InferableComponentEnhancerWithProps< + TStateProps & TDispatchProps, + TOwnProps + > + + < + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {} + //State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam + ): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps + > + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps + ): InferableComponentEnhancerWithProps + + < + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {} + //State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps + ): InferableComponentEnhancerWithProps + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps + ): InferableComponentEnhancerWithProps + + < + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {} + //State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: null | undefined, + options: Options + ): InferableComponentEnhancerWithProps< + TypedDispatchProp & TStateProps, + TOwnProps + > + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: Options<{}, TStateProps, TOwnProps> + ): InferableComponentEnhancerWithProps + + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: Options<{}, TStateProps, TOwnProps> + ): InferableComponentEnhancerWithProps< + ResolveThunks, + TOwnProps + > + + < + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {} + //State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: Options + ): InferableComponentEnhancerWithProps< + TStateProps & TDispatchProps, + TOwnProps + > + + < + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {} + //State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: Options + ): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps + > + + < + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {} + //State = DefaultRootState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps + >, + options?: Options + ): InferableComponentEnhancerWithProps +} + +type ConnectAdvanced = { + ( + selectorFactory: SelectorFactory, + connectOptions?: ConnectOptions & TFactoryOptions + ): AdvancedComponentDecorator +} diff --git a/src/index.ts b/src/index.ts index d33fc5f553..5f716e25d1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -105,5 +105,7 @@ export { unwrapResult, SerializedError } from './createAsyncThunk' +export { createThunk } from './createThunk' +export { curryForStoreType } from './curriedTypes' export { nanoid } from './nanoid' diff --git a/src/tsHelpers.ts b/src/tsHelpers.ts index 43ec40a3af..4935516a97 100644 --- a/src/tsHelpers.ts +++ b/src/tsHelpers.ts @@ -81,10 +81,22 @@ export type DispatchForMiddlewares = M extends ReadonlyArray > : never +export type ExtraFromMiddlewares< + MW extends ReadonlyArray +> = MW[number] extends infer M + ? M extends Middleware + ? DispatchExt extends ( + _: (__: any, ___: any, ____: infer Extra) => any + ) => any + ? Extra + : never + : never + : never + /** * Convert a Union type `(A|B)` to and intersecion type `(A&B)` */ -type UnionToIntersection = (U extends any +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I diff --git a/type-tests/files/curriedTypes.typetest.ts b/type-tests/files/curriedTypes.typetest.ts new file mode 100644 index 0000000000..801f0d425a --- /dev/null +++ b/type-tests/files/curriedTypes.typetest.ts @@ -0,0 +1,103 @@ +import { useDispatch, useSelector } from 'react-redux' +import { + configureStore, + createAsyncThunk, + createThunk, + Middleware, + curryForStoreType +} from 'src' + +type DispatchType = typeof store.dispatch +type StateType = ReturnType + +const otherMiddleware: Middleware< + { (arg: number): number }, + any, + any +> = _ => next => action => next(action) + +const store = configureStore({ + reducer(state?: { foo: 'bar' }) { + return state! + }, + middleware: getDefaultMiddleware => + getDefaultMiddleware({ + thunk: { extraArgument: 'thunkExtra' as const } + }).concat(otherMiddleware) +}) + +const curryTypes = curryForStoreType() + +function expectType(t: T) { + return t +} +// currying `useDispatch` +{ + expectType<() => DispatchType>(curryTypes(useDispatch)) +} + +// currying `useSelector` +{ + expectType< + ( + selector: (state: StateType) => X, + equalityFn?: (x1: X, x2: X) => boolean + ) => any + >(curryTypes(useSelector)) +} + +/** + * currying a lot of stuff + */ +{ + const { + useDispatch: useAppDispatch, + useSelector: useAppSelector + } = curryTypes({ + useDispatch, + useSelector + }) + + expectType<() => DispatchType>(useAppDispatch) + + // typings:expect-error does not accept wrong types for a key + curryTypes({ useDispatch: () => {} }) + + // typings:expect-error will not have stuff in the output that is not in the input + const { useDispatch: notThere } = curryTypes({}) +} + +/** + * currying all supported exports of react-redux at once + */ +{ + const { useDispatch, useSelector, connect, connectAdvanced } = curryTypes( + require('react-redux') + ) +} +/** + * currying `createAsyncThunk` + */ +{ + const curriedCAT = curryTypes(createAsyncThunk) + curriedCAT('foo', (arg: any, { getState, dispatch, extra }) => { + expectType(dispatch) + expectType(getState()) + expectType<'thunkExtra'>(extra) + }) +} +/** + * currying `createThunk` + */ +{ + const curriedCT = curryTypes(createThunk) + + const thunk = curriedCT( + (arg1: string, arg2: number) => (dispatch, getState, extra) => { + expectType(dispatch) + expectType(getState()) + expectType<'thunkExtra'>(extra) + return null + } + ) +}