diff --git a/package.json b/package.json index f1d9fd12b..788b7ff37 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "prepare": "yarn clean && yarn build", "pretest": "yarn lint", "test": "jest", - "type-tests": "yarn tsc -p test/typetests", + "type-tests": "yarn tsc -p test/typetests/tsconfig.json", "coverage": "codecov" }, "peerDependencies": { diff --git a/src/components/Provider.tsx b/src/components/Provider.tsx index 8a95454c7..f1c12fe10 100644 --- a/src/components/Provider.tsx +++ b/src/components/Provider.tsx @@ -2,24 +2,27 @@ import React, { Context, ReactNode, useMemo } from 'react' import { ReactReduxContext, ReactReduxContextValue } from './Context' import { createSubscription } from '../utils/Subscription' import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect' -import type { FixTypeLater } from '../types' import { Action, AnyAction, Store } from 'redux' export interface ProviderProps { /** * The single Redux store in your application. */ - store: Store + store: Store /** * Optional context to be used internally in react-redux. Use React.createContext() to create a context to be used. * If this is used, you'll need to customize `connect` by supplying the same context provided to the Provider. * Initial value doesn't matter, as it is overwritten with the internal state of Provider. */ - context?: Context + context?: Context> children: ReactNode } -function Provider({ store, context, children }: ProviderProps) { +function Provider({ + store, + context, + children, +}: ProviderProps) { const contextValue = useMemo(() => { const subscription = createSubscription(store) return { @@ -46,6 +49,7 @@ function Provider({ store, context, children }: ProviderProps) { const Context = context || ReactReduxContext + // @ts-ignore 'AnyAction' is assignable to the constraint of type 'A', but 'A' could be instantiated with a different subtype return {children} } diff --git a/src/components/connect.tsx b/src/components/connect.tsx index a3f4d9317..f4eb8da80 100644 --- a/src/components/connect.tsx +++ b/src/components/connect.tsx @@ -192,11 +192,16 @@ function subscribeUpdates( const initStateUpdates = () => EMPTY_ARRAY export interface ConnectProps { - reactReduxForwardedRef?: React.ForwardedRef + /** A custom Context instance that the component can use to access the store from an alternate Provider using that same Context instance */ context?: ReactReduxContextInstance + /** A Redux store instance to be used for subscriptions instead of the store from a Provider */ store?: Store } +interface InternalConnectProps extends ConnectProps { + reactReduxForwardedRef?: React.ForwardedRef +} + function match( arg: unknown, factories: ((value: unknown) => T)[], @@ -260,183 +265,163 @@ export interface ConnectOptions< ) => boolean } -/* @public */ -function connect(): InferableComponentEnhancer +/** + * Connects a React component to a Redux store. + * + * - Without arguments, just wraps the component, without changing the behavior / props + * + * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior + * is to override ownProps (as stated in the docs), so what remains is everything that's + * not a state or dispatch prop + * + * - When 3rd param is passed, we don't know if ownProps propagate and whether they + * should be valid component props, because it depends on mergeProps implementation. + * As such, it is the user's responsibility to extend ownProps interface from state or + * dispatch props or both when applicable + * + * @param mapStateToProps + * @param mapDispatchToProps + * @param mergeProps + * @param options + */ +export interface Connect { + // tslint:disable:no-unnecessary-generics + (): InferableComponentEnhancer + + /** mapState only */ + ( + mapStateToProps: MapStateToPropsParam + ): InferableComponentEnhancerWithProps + + /** mapDispatch only (as a function) */ + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject + ): InferableComponentEnhancerWithProps + + /** mapDispatch only (as an object) */ + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam + ): InferableComponentEnhancerWithProps< + ResolveThunks, + TOwnProps + > -/* @public */ -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam -): InferableComponentEnhancerWithProps< - TStateProps & DispatchProp, - TOwnProps & ConnectProps -> - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam -): InferableComponentEnhancerWithProps< - ResolveThunks, - TOwnProps & ConnectProps -> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject -): InferableComponentEnhancerWithProps< - TStateProps & TDispatchProps, - TOwnProps & ConnectProps -> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam -): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps & ConnectProps -> - -/* @public */ -function connect< - no_state = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {} ->( - mapStateToProps: null | undefined, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps + /** mapState and mapDispatch (as a function)*/ + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject + ): InferableComponentEnhancerWithProps< + TStateProps & TDispatchProps, + TOwnProps + > -/* @public */ -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps + /** mapState and mapDispatch (as an object) */ + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam + ): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps + > -/* @public */ -function connect< - no_state = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {} ->( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: MergeProps -): InferableComponentEnhancerWithProps + /** mergeProps only */ + ( + mapStateToProps: null | undefined, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps + ): InferableComponentEnhancerWithProps + + /** mapState and mergeProps */ + < + TStateProps = {}, + no_dispatch = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: MergeProps + ): InferableComponentEnhancerWithProps + + /** mapDispatch (as a object) and mergeProps */ + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps + ): InferableComponentEnhancerWithProps + + /** mapState and options */ + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: null | undefined, + mergeProps: null | undefined, + options: ConnectOptions + ): InferableComponentEnhancerWithProps + + /** mapDispatch (as a function) and options */ + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> + ): InferableComponentEnhancerWithProps + + /** mapDispatch (as an object) and options*/ + ( + mapStateToProps: null | undefined, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions<{}, TStateProps, TOwnProps> + ): InferableComponentEnhancerWithProps< + ResolveThunks, + TOwnProps + > -/* @public */ -// @ts-ignore -function connect< - TStateProps = {}, - no_dispatch = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: null | undefined, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps< - DispatchProp & TStateProps, - TOwnProps & ConnectProps -> - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> -): InferableComponentEnhancerWithProps - -/* @public */ -function connect( - mapStateToProps: null | undefined, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions<{}, TStateProps, TOwnProps> -): InferableComponentEnhancerWithProps< - ResolveThunks, - TOwnProps & ConnectProps -> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsNonObject, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps< - TStateProps & TDispatchProps, - TOwnProps & ConnectProps -> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - State = DefaultRootState ->( - mapStateToProps: MapStateToPropsParam, - mapDispatchToProps: MapDispatchToPropsParam, - mergeProps: null | undefined, - options: ConnectOptions -): InferableComponentEnhancerWithProps< - TStateProps & ResolveThunks, - TOwnProps & ConnectProps -> - -/* @public */ -function connect< - TStateProps = {}, - TDispatchProps = {}, - TOwnProps = {}, - TMergedProps = {}, - State = DefaultRootState ->( - mapStateToProps?: MapStateToPropsParam, - mapDispatchToProps?: MapDispatchToPropsParam, - mergeProps?: MergeProps, - options?: ConnectOptions -): InferableComponentEnhancerWithProps + /** mapState, mapDispatch (as a function), and options */ + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsNonObject, + mergeProps: null | undefined, + options: ConnectOptions + ): InferableComponentEnhancerWithProps< + TStateProps & TDispatchProps, + TOwnProps + > + + /** mapState, mapDispatch (as an object), and options */ + ( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: null | undefined, + options: ConnectOptions + ): InferableComponentEnhancerWithProps< + TStateProps & ResolveThunks, + TOwnProps + > + + /** mapState, mapDispatch, mergeProps, and options */ + < + TStateProps = {}, + TDispatchProps = {}, + TOwnProps = {}, + TMergedProps = {}, + State = DefaultState + >( + mapStateToProps: MapStateToPropsParam, + mapDispatchToProps: MapDispatchToPropsParam, + mergeProps: MergeProps< + TStateProps, + TDispatchProps, + TOwnProps, + TMergedProps + >, + options?: ConnectOptions + ): InferableComponentEnhancerWithProps + // tslint:enable:no-unnecessary-generics +} /** * Connects a React component to a Redux store. @@ -558,7 +543,9 @@ function connect< // that just executes the given callback immediately. const usePureOnlyMemo = pure ? useMemo : (callback: () => any) => callback() - function ConnectFunction(props: ConnectProps & TOwnProps) { + function ConnectFunction( + props: InternalConnectProps & TOwnProps + ) { const [propsContext, reactReduxForwardedRef, wrapperProps] = useMemo(() => { // Distinguish between actual "data" props that were passed to the wrapper component, @@ -826,4 +813,4 @@ function connect< return wrapWithConnect } -export default connect +export default connect as Connect diff --git a/src/connect/selectorFactory.ts b/src/connect/selectorFactory.ts index 1dfea1610..5321ac517 100644 --- a/src/connect/selectorFactory.ts +++ b/src/connect/selectorFactory.ts @@ -231,14 +231,12 @@ export default function finalPropsSelectorFactory< verifySubselectors(mapStateToProps, mapDispatchToProps, mergeProps) } - const selectorFactory = pureFinalPropsSelectorFactory - - return selectorFactory( + return pureFinalPropsSelectorFactory< + TStateProps, + TOwnProps, + TDispatchProps, + TMergedProps, + State // @ts-ignore - mapStateToProps!, - mapDispatchToProps, - mergeProps, - dispatch, - options - ) + >(mapStateToProps!, mapDispatchToProps, mergeProps, dispatch, options) } diff --git a/src/exports.ts b/src/exports.ts index 94fd09b03..72a2e95c2 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,6 +1,11 @@ import Provider from './components/Provider' import type { ProviderProps } from './components/Provider' -import connect, { ConnectProps, ConnectedProps } from './components/connect' +import connect from './components/connect' +import type { + Connect, + ConnectProps, + ConnectedProps, +} from './components/connect' import type { SelectorFactory, Selector, @@ -32,6 +37,7 @@ export type { MapStateToProps, MapStateToPropsFactory, MapStateToPropsParam, + Connect, ConnectProps, ConnectedProps, MapDispatchToPropsFunction, diff --git a/src/hooks/useStore.ts b/src/hooks/useStore.ts index 85fd238d8..e1cfb9be0 100644 --- a/src/hooks/useStore.ts +++ b/src/hooks/useStore.ts @@ -1,6 +1,11 @@ -import { useContext } from 'react' -import { ReactReduxContext } from '../components/Context' +import { useContext, Context } from 'react' +import { Action as BasicAction, AnyAction, Store } from 'redux' +import { + ReactReduxContext, + ReactReduxContextValue, +} from '../components/Context' import { useReduxContext as useDefaultReduxContext } from './useReduxContext' +import { RootStateOrAny } from '../types' /** * Hook factory, which creates a `useStore` hook bound to a given context. @@ -8,14 +13,24 @@ import { useReduxContext as useDefaultReduxContext } from './useReduxContext' * @param {React.Context} [context=ReactReduxContext] Context passed to your ``. * @returns {Function} A `useStore` hook bound to the specified context. */ -export function createStoreHook(context = ReactReduxContext) { +export function createStoreHook< + S = RootStateOrAny, + A extends BasicAction = AnyAction + // @ts-ignore +>(context?: Context> = ReactReduxContext) { const useReduxContext = + // @ts-ignore context === ReactReduxContext ? useDefaultReduxContext : () => useContext(context) - return function useStore() { + return function useStore< + State = S, + Action extends BasicAction = A + // @ts-ignore + >() { const { store } = useReduxContext()! - return store + // @ts-ignore + return store as Store } } diff --git a/src/types.ts b/src/types.ts index 4fff99c46..23c31801e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,14 @@ -/* eslint-disable no-unused-vars */ -// TODO Ignoring all unused variables for now - import { ClassAttributes, ComponentClass, ComponentType } from 'react' import { Action, AnyAction, Dispatch } from 'redux' -// import hoistNonReactStatics = require('hoist-non-react-statics'); import type { NonReactStatics } from 'hoist-non-react-statics' +import type { ConnectProps } from './components/connect' + export type FixTypeLater = any -export type EqualityFn = (a: T | undefined, b: T | undefined) => boolean +export type EqualityFn = (a: T, b: T) => boolean /** * This interface can be augmented by users to add default types for the root state when @@ -24,9 +22,6 @@ export interface DefaultRootState {} export type AnyIfEmpty = keyof T extends never ? any : T export type RootStateOrAny = AnyIfEmpty -// Omit taken from https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html -export type Omit = Pick> - export type DistributiveOmit = T extends unknown ? Omit : never @@ -85,6 +80,13 @@ export type GetProps = C extends ComponentType : P : never +// Applies LibraryManagedAttributes (proper handling of defaultProps +// and propTypes). +export type GetLibraryManagedProps = JSX.LibraryManagedAttributes< + C, + GetProps +> + // Applies LibraryManagedAttributes (proper handling of defaultProps // and propTypes), as well as defines WrappedComponent. export type ConnectedComponent< @@ -105,8 +107,12 @@ export type InferableComponentEnhancerWithProps = < component: C ) => ConnectedComponent< C, - DistributiveOmit, keyof Shared>> & - TNeedsProps + DistributiveOmit< + GetLibraryManagedProps, + keyof Shared> + > & + TNeedsProps & + ConnectProps > // Injects props and removes them from the prop requirements. @@ -139,116 +145,6 @@ export type ResolveThunks = TDispatchProps extends { } : TDispatchProps -// the conditional type is to support TypeScript 3.0, which does not support mapping over tuples and arrays; -// once the typings are updated to at least TypeScript 3.1, a simple mapped type can replace this mess -export type ResolveArrayThunks> = - TDispatchProps extends [ - infer A1, - infer A2, - infer A3, - infer A4, - infer A5, - infer A6, - infer A7, - infer A8, - infer A9 - ] - ? [ - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator - ] - : TDispatchProps extends [ - infer A1, - infer A2, - infer A3, - infer A4, - infer A5, - infer A6, - infer A7, - infer A8 - ] - ? [ - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator - ] - : TDispatchProps extends [ - infer A1, - infer A2, - infer A3, - infer A4, - infer A5, - infer A6, - infer A7 - ] - ? [ - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator - ] - : TDispatchProps extends [ - infer A1, - infer A2, - infer A3, - infer A4, - infer A5, - infer A6 - ] - ? [ - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator - ] - : TDispatchProps extends [infer A1, infer A2, infer A3, infer A4, infer A5] - ? [ - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator - ] - : TDispatchProps extends [infer A1, infer A2, infer A3, infer A4] - ? [ - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator - ] - : TDispatchProps extends [infer A1, infer A2, infer A3] - ? [ - HandleThunkActionCreator, - HandleThunkActionCreator, - HandleThunkActionCreator - ] - : TDispatchProps extends [infer A1, infer A2] - ? [HandleThunkActionCreator, HandleThunkActionCreator] - : TDispatchProps extends [infer A1] - ? [HandleThunkActionCreator] - : TDispatchProps extends Array - ? Array> - : TDispatchProps extends ReadonlyArray - ? ReadonlyArray> - : never - /** * This interface allows you to easily create a hook that is properly typed for your * store's root state. diff --git a/test/components/connect.spec.tsx b/test/components/connect.spec.tsx index 27cf71b32..f28f0be05 100644 --- a/test/components/connect.spec.tsx +++ b/test/components/connect.spec.tsx @@ -2181,7 +2181,7 @@ describe('React', () => { const decorator = connect((state) => { actualState = state - return {} + return { a: 42 } }) const Decorated = decorator(Container) @@ -2829,45 +2829,50 @@ describe('React', () => { } it('should throw a helpful error for invalid mapStateToProps arguments', () => { - //@ts-expect-error - @connect('invalid') class InvalidMapState extends React.Component { render() { return
} } - const error = renderWithBadConnect(InvalidMapState) + //@ts-expect-error + // eslint-disable-next-line + const Connected = connect('invalid')(InvalidMapState) + + const error = renderWithBadConnect(Connected) expect(error).toContain('string') expect(error).toContain('mapStateToProps') expect(error).toContain('InvalidMapState') }) it('should throw a helpful error for invalid mapDispatchToProps arguments', () => { - //@ts-expect-error - @connect(null, 'invalid') class InvalidMapDispatch extends React.Component { render() { return
} } - const error = renderWithBadConnect(InvalidMapDispatch) + // eslint-disable-next-line + const Connected = connect(null, 'invalid')(InvalidMapDispatch) + + const error = renderWithBadConnect(Connected) expect(error).toContain('string') expect(error).toContain('mapDispatchToProps') expect(error).toContain('InvalidMapDispatch') }) it('should throw a helpful error for invalid mergeProps arguments', () => { - // @ts-expect-error - @connect(null, null, 'invalid') class InvalidMerge extends React.Component { render() { return
} } - const error = renderWithBadConnect(InvalidMerge) + // @ts-expect-error + // eslint-disable-next-line + const Connected = connect(null, null, 'invalid')(InvalidMerge) + + const error = renderWithBadConnect(Connected) expect(error).toContain('string') expect(error).toContain('mergeProps') expect(error).toContain('InvalidMerge') diff --git a/test/hooks/useSelector.spec.tsx b/test/hooks/useSelector.spec.tsx index 20c354708..59cf034cb 100644 --- a/test/hooks/useSelector.spec.tsx +++ b/test/hooks/useSelector.spec.tsx @@ -1,7 +1,6 @@ /*eslint-disable react/prop-types*/ import React, { useCallback, useReducer, useLayoutEffect } from 'react' -import ReactDOM from 'react-dom' import { createStore } from 'redux' import * as rtl from '@testing-library/react' import { diff --git a/test/tsconfig.test.json b/test/tsconfig.test.json index 772b7a17f..461da76db 100644 --- a/test/tsconfig.test.json +++ b/test/tsconfig.test.json @@ -1,17 +1,18 @@ { - "extends": "../tsconfig.json", "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", "emitDeclarationOnly": false, + "declaration": false, "strict": true, "noEmit": true, "target": "es2018", "jsx": "react", "baseUrl": ".", "skipLibCheck": true, - "noImplicitReturns": false + "noImplicitReturns": false, + "experimentalDecorators": true, } } diff --git a/test/typetests/connect-mapstate-mapdispatch.tsx b/test/typetests/connect-mapstate-mapdispatch.tsx new file mode 100644 index 000000000..946f62181 --- /dev/null +++ b/test/typetests/connect-mapstate-mapdispatch.tsx @@ -0,0 +1,459 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, no-inner-declarations */ + +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { + Store, + Dispatch, + AnyAction, + ActionCreator, + createStore, + bindActionCreators, + ActionCreatorsMapObject, + Reducer, +} from 'redux' +import { + connect, + ConnectedProps, + Provider, + DispatchProp, + MapStateToProps, + ReactReduxContext, + ReactReduxContextValue, + Selector, + shallowEqual, + MapDispatchToProps, + useDispatch, + useSelector, + useStore, + createDispatchHook, + createSelectorHook, + createStoreHook, + TypedUseSelectorHook, +} from '../../src/index' + +// Test cases written in a way to isolate types and variables and verify the +// output of `connect` to make sure the signature is what is expected + +const CustomContext = React.createContext( + null +) as unknown as typeof ReactReduxContext + +function Empty() { + interface OwnProps { + dispatch: Dispatch + foo: string + } + + class TestComponent extends React.Component {} + + const Test = connect()(TestComponent) + + const verify = +} + +function MapState() { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + + class TestComponent extends React.Component {} + + const mapStateToProps = (_: any) => ({ + bar: 1, + }) + + const Test = connect(mapStateToProps)(TestComponent) + + const verify = +} + +function MapStateWithDispatchProp() { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + dispatch: Dispatch + } + + class TestComponent extends React.Component {} + + const mapStateToProps = (_: any) => ({ + bar: 1, + }) + + const Test = connect(mapStateToProps)(TestComponent) + + const verify = +} + +function MapStateFactory() { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + + class TestComponent extends React.Component {} + + const mapStateToProps = () => () => ({ + bar: 1, + }) + + const Test = connect(mapStateToProps)(TestComponent) + + const verify = +} + +function MapDispatch() { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + const mapDispatchToProps = { onClick: () => {} } + + const TestNull = connect(null, mapDispatchToProps)(TestComponent) + + const verifyNull = + + const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) + + const verifyUndefined = +} + +function MapDispatchUnion() { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + // We deliberately cast the right-hand side to `any` because otherwise + // TypeScript would maintain the literal value, when we deliberately want to + // test the union type here (as per the annotation). See + // https://github.com/Microsoft/TypeScript/issues/30310#issuecomment-472218182. + const mapDispatchToProps: MapDispatchToProps = + {} as any + + const TestNull = connect(null, mapDispatchToProps)(TestComponent) + + const verifyNull = + + const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) + + const verifyUndefined = +} + +function MapDispatchWithThunkActionCreators() { + const simpleAction = (payload: boolean) => ({ + type: 'SIMPLE_ACTION', + payload, + }) + const thunkAction = + (param1: number, param2: string) => + async (dispatch: Dispatch, { foo }: OwnProps) => { + return foo + } + interface OwnProps { + foo: string + } + interface TestComponentProps extends OwnProps { + simpleAction: typeof simpleAction + thunkAction(param1: number, param2: string): Promise + } + class TestComponent extends React.Component {} + + const mapStateToProps = ({ foo }: { foo: string }) => ({ foo }) + const mapDispatchToProps = { simpleAction, thunkAction } + + const Test1 = connect(null, mapDispatchToProps)(TestComponent) + const Test2 = connect(mapStateToProps, mapDispatchToProps)(TestComponent) + const Test3 = connect(null, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const Test4 = connect(mapStateToProps, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const verify = ( +
+ ; + + ; + +
+ ) +} + +function MapManualDispatchThatLooksLikeThunk() { + interface OwnProps { + foo: string + } + interface TestComponentProps extends OwnProps { + remove: (item: string) => () => object + } + class TestComponent extends React.Component { + render() { + return
+ } + } + + const mapStateToProps = ({ foo }: { foo: string }) => ({ foo }) + function mapDispatchToProps(dispatch: Dispatch) { + return { + remove(item: string) { + return () => dispatch({ type: 'REMOVE_ITEM', item }) + }, + } + } + + const Test1 = connect(null, mapDispatchToProps)(TestComponent) + const Test2 = connect(mapStateToProps, mapDispatchToProps)(TestComponent) + const Test3 = connect(null, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const Test4 = connect(mapStateToProps, mapDispatchToProps, null, { + context: CustomContext, + })(TestComponent) + const verify = ( +
+ ; + + ; + +
+ ) +} + +function MapStateAndDispatchObject() { + interface ClickPayload { + count: number + } + const onClick: ActionCreator = () => ({ count: 1 }) + const dispatchToProps = { + onClick, + } + + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: ActionCreator + } + + const mapStateToProps = (_: any, __: OwnProps): StateProps => ({ + bar: 1, + }) + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const Test = connect(mapStateToProps, dispatchToProps)(TestComponent) + + const verify = +} + +function MapDispatchFactory() { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + const mapDispatchToPropsFactory = () => () => ({ + onClick: () => {}, + }) + + const TestNull = connect(null, mapDispatchToPropsFactory)(TestComponent) + + const verifyNull = + + const TestUndefined = connect( + undefined, + mapDispatchToPropsFactory + )(TestComponent) + + const verifyUndefined = +} + +function MapStateAndDispatch() { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const mapStateToProps = () => ({ + bar: 1, + }) + + const mapDispatchToProps = () => ({ + onClick: () => {}, + }) + + const Test = connect(mapStateToProps, mapDispatchToProps)(TestComponent) + + const verify = +} + +function MapStateFactoryAndDispatch() { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + const mapStateToPropsFactory = () => () => ({ + bar: 1, + }) + + const mapDispatchToProps = () => ({ + onClick: () => {}, + }) + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const Test = connect( + mapStateToPropsFactory, + mapDispatchToProps + )(TestComponent) + + const verify = +} + +function MapStateFactoryAndDispatchFactory() { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + const mapStateToPropsFactory = () => () => ({ + bar: 1, + }) + + const mapDispatchToPropsFactory = () => () => ({ + onClick: () => {}, + }) + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const Test = connect( + mapStateToPropsFactory, + mapDispatchToPropsFactory + )(TestComponent) + + const verify = +} + +function MapStateAndDispatchAndMerge() { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const mapStateToProps = () => ({ + bar: 1, + }) + + const mapDispatchToProps = () => ({ + onClick: () => {}, + }) + + const mergeProps = ( + stateProps: StateProps, + dispatchProps: DispatchProps + ) => ({ ...stateProps, ...dispatchProps }) + + const Test = connect( + mapStateToProps, + mapDispatchToProps, + mergeProps + )(TestComponent) + + const verify = +} + +function MapStateAndOptions() { + interface State { + state: string + } + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + dispatch: Dispatch + } + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const mapStateToProps = (state: State) => ({ + bar: 1, + }) + + const areStatePropsEqual = (next: StateProps, current: StateProps) => true + + const Test = connect( + mapStateToProps, + null, + null, + { + areStatePropsEqual, + } + )(TestComponent) + + const verify = +} diff --git a/test/typetests/connect-options-and-issues.tsx b/test/typetests/connect-options-and-issues.tsx new file mode 100644 index 000000000..7cb319070 --- /dev/null +++ b/test/typetests/connect-options-and-issues.tsx @@ -0,0 +1,876 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, react/prop-types */ +import * as PropTypes from 'prop-types' +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { + Store, + Dispatch, + AnyAction, + ActionCreator, + createStore, + bindActionCreators, + ActionCreatorsMapObject, + Reducer, +} from 'redux' +import { + connect, + Connect, + ConnectedProps, + Provider, + DispatchProp, + MapStateToProps, + ReactReduxContext, + ReactReduxContextValue, + Selector, + shallowEqual, + MapDispatchToProps, + useDispatch, + useSelector, + useStore, + createDispatchHook, + createSelectorHook, + createStoreHook, + TypedUseSelectorHook, + DefaultRootState, +} from '../../src/index' + +import { expectType } from '../typeTestHelpers' + +// Test cases written in a way to isolate types and variables and verify the +// output of `connect` to make sure the signature is what is expected + +const CustomContext = React.createContext( + null +) as unknown as typeof ReactReduxContext + +// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16021 +function TestMergedPropsInference() { + interface StateProps { + state: string + } + + interface DispatchProps { + dispatch: string + } + + interface OwnProps { + own: string + } + + interface MergedProps { + merged: string + } + + class MergedPropsComponent extends React.Component { + render() { + return
+ } + } + + function mapStateToProps(state: any): StateProps { + return { state: 'string' } + } + + function mapDispatchToProps(dispatch: Dispatch): DispatchProps { + return { dispatch: 'string' } + } + + const ConnectedWithOwnAndState = connect< + StateProps, + void, + OwnProps, + MergedProps + >(mapStateToProps, undefined, (stateProps: StateProps) => ({ + merged: 'merged', + }))(MergedPropsComponent) + + const ConnectedWithOwnAndDispatch = connect< + void, + DispatchProps, + OwnProps, + MergedProps + >( + undefined, + mapDispatchToProps, + (stateProps: undefined, dispatchProps: DispatchProps) => ({ + merged: 'merged', + }) + )(MergedPropsComponent) + + const ConnectedWithOwn = connect( + undefined, + undefined, + () => ({ + merged: 'merged', + }) + )(MergedPropsComponent) +} + +function Issue16652() { + interface PassedProps { + commentIds: string[] + } + + interface GeneratedStateProps { + comments: Array<{ id: string } | undefined> + } + + class CommentList extends React.Component< + PassedProps & GeneratedStateProps & DispatchProp + > {} + + const mapStateToProps = ( + state: any, + ownProps: PassedProps + ): GeneratedStateProps => { + return { + comments: ownProps.commentIds.map((id) => ({ id })), + } + } + + const ConnectedCommentList = connect( + mapStateToProps + )(CommentList) + + ; +} + +function Issue15463() { + interface SpinnerProps { + showGlobalSpinner: boolean + } + + class SpinnerClass extends React.Component { + render() { + return
+ } + } + + const Spinner = connect((state: any) => { + return { showGlobalSpinner: true } + })(SpinnerClass) + + ; +} + +function RemoveInjectedAndPassOnRest() { + interface TProps { + showGlobalSpinner: boolean + foo: string + } + class SpinnerClass extends React.Component { + render() { + return
+ } + } + + const Spinner = connect((state: any) => { + return { showGlobalSpinner: true } + })(SpinnerClass) + + ; +} + +function TestControlledComponentWithoutDispatchProp() { + interface MyState { + count: number + } + + interface MyProps { + label: string + // `dispatch` is optional, but setting it to anything + // other than Dispatch will cause an error + // + // dispatch: Dispatch; // OK + // dispatch: number; // ERROR + } + + function mapStateToProps(state: MyState) { + return { + label: `The count is ${state.count}`, + } + } + + class MyComponent extends React.Component { + render() { + return {this.props.label} + } + } + + const MyFuncComponent = (props: MyProps) => {props.label} + + const MyControlledComponent = connect(mapStateToProps)(MyComponent) + const MyControlledFuncComponent = connect(mapStateToProps)(MyFuncComponent) +} + +function TestDispatchToPropsAsObject() { + const onClick: ActionCreator<{}> = () => ({}) + const mapStateToProps = (state: any) => { + return { + title: state.app.title as string, + } + } + const dispatchToProps = { + onClick, + } + + type Props = { title: string } & typeof dispatchToProps + const HeaderComponent: React.FunctionComponent = (props) => { + return

{props.title}

+ } + + const Header = connect(mapStateToProps, dispatchToProps)(HeaderComponent) + ;
+} + +function TestInferredFunctionalComponentWithExplicitOwnProps() { + interface Props { + title: string + extraText: string + onClick: () => void + } + + const Header = connect( + ( + { app: { title } }: { app: { title: string } }, + { extraText }: { extraText: string } + ) => ({ + title, + extraText, + }), + (dispatch) => ({ + onClick: () => dispatch({ type: 'test' }), + }) + )(({ title, extraText, onClick }: Props) => { + return ( +

+ {title} {extraText} +

+ ) + }) + ;
+} + +function TestInferredFunctionalComponentWithImplicitOwnProps() { + interface Props { + title: string + extraText: string + onClick: () => void + } + + const Header = connect( + ({ app: { title } }: { app: { title: string } }) => ({ + title, + }), + (dispatch) => ({ + onClick: () => dispatch({ type: 'test' }), + }) + )(({ title, extraText, onClick }: Props) => { + return ( +

+ {title} {extraText} +

+ ) + }) + ;
+} + +function TestWrappedComponent() { + interface InnerProps { + name: string + } + const Inner: React.FunctionComponent = (props) => { + return

{props.name}

+ } + + const mapStateToProps = (state: any) => { + return { + name: 'Connected', + } + } + const Connected = connect(mapStateToProps)(Inner) + + // `Inner` and `Connected.WrappedComponent` require explicit `name` prop + const TestInner = (props: any) => + const TestWrapped = (props: any) => ( + + ) + // `Connected` does not require explicit `name` prop + const TestConnected = (props: any) => +} + +function TestWithoutTOwnPropsDecoratedInference() { + interface ForwardedProps { + forwarded: string + } + + interface OwnProps { + own: string + } + + interface StateProps { + state: string + } + + class WithoutOwnPropsComponentClass extends React.Component< + ForwardedProps & StateProps & DispatchProp + > { + render() { + return
+ } + } + + const WithoutOwnPropsComponentStateless: React.FunctionComponent< + ForwardedProps & StateProps & DispatchProp + > = () =>
+ + function mapStateToProps4(state: any, ownProps: OwnProps): StateProps { + return { state: 'string' } + } + + // these decorations should compile, it is perfectly acceptable to receive props and ignore them + const ConnectedWithOwnPropsClass = connect(mapStateToProps4)( + WithoutOwnPropsComponentClass + ) + const ConnectedWithOwnPropsStateless = connect(mapStateToProps4)( + WithoutOwnPropsComponentStateless + ) + const ConnectedWithTypeHintClass = connect( + mapStateToProps4 + )(WithoutOwnPropsComponentClass) + const ConnectedWithTypeHintStateless = connect( + mapStateToProps4 + )(WithoutOwnPropsComponentStateless) + + // This should compile + React.createElement(ConnectedWithOwnPropsClass, { + own: 'string', + forwarded: 'string', + }) + React.createElement(ConnectedWithOwnPropsClass, { + own: 'string', + forwarded: 'string', + }) + + // This should not compile, it is missing ForwardedProps + // @ts-expect-error + React.createElement(ConnectedWithOwnPropsClass, { own: 'string' }) + // @ts-expect-error + React.createElement(ConnectedWithOwnPropsStateless, { own: 'string' }) + + // This should compile + React.createElement(ConnectedWithOwnPropsClass, { + own: 'string', + forwarded: 'string', + }) + React.createElement(ConnectedWithOwnPropsStateless, { + own: 'string', + forwarded: 'string', + }) + + // This should not compile, it is missing ForwardedProps + // @ts-expect-error + React.createElement(ConnectedWithTypeHintClass, { own: 'string' }) + // @ts-expect-error + React.createElement(ConnectedWithTypeHintStateless, { own: 'string' }) + + interface AllProps { + own: string + state: string + } + + class AllPropsComponent extends React.Component< + AllProps & DispatchProp + > { + render() { + return
+ } + } + + type PickedOwnProps = Pick + type PickedStateProps = Pick + + const mapStateToPropsForPicked: MapStateToProps< + PickedStateProps, + PickedOwnProps, + {} + > = (state: any): PickedStateProps => { + return { state: 'string' } + } + const ConnectedWithPickedOwnProps = connect(mapStateToPropsForPicked)( + AllPropsComponent + ) + ; +} + +// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/25321#issuecomment-387659500 +function ProviderAcceptsStoreWithCustomAction() { + const reducer: Reducer< + { foo: number } | undefined, + { type: 'foo'; payload: number } + > = (state) => state + + const store = createStore(reducer) + + const Whatever = () => ( + +
Whatever
+
+ ) +} + +function TestOptionalPropsMergedCorrectly() { + interface OptionalDecorationProps { + foo: string + bar: number + optionalProp?: boolean | undefined + dependsOnDispatch?: (() => void) | undefined + } + + class Component extends React.Component { + render() { + return
+ } + } + + interface MapStateProps { + foo: string + bar: number + optionalProp: boolean + } + + interface MapDispatchProps { + dependsOnDispatch: () => void + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + optionalProp: true, + } + } + + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } + + connect(mapStateToProps, mapDispatchToProps)(Component) +} + +function TestMoreGeneralDecorationProps() { + // connect() should support decoration props that are more permissive + // than the injected props, as long as the injected props can satisfy + // the decoration props. + interface MoreGeneralDecorationProps { + foo: string | number + bar: number | 'foo' + optionalProp?: boolean | object | undefined + dependsOnDispatch?: (() => void) | undefined + } + + class Component extends React.Component { + render() { + return
+ } + } + + interface MapStateProps { + foo: string + bar: number + optionalProp: boolean + } + + interface MapDispatchProps { + dependsOnDispatch: () => void + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + optionalProp: true, + } + } + + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } + + connect(mapStateToProps, mapDispatchToProps)(Component) +} + +function TestFailsMoreSpecificInjectedProps() { + interface MoreSpecificDecorationProps { + foo: string + bar: number + dependsOnDispatch: () => void + } + + class Component extends React.Component { + render() { + return
+ } + } + + interface MapStateProps { + foo: string | number + bar: number | 'foo' + dependsOnDispatch?: (() => void) | undefined + } + + interface MapDispatchProps { + dependsOnDispatch?: (() => void) | undefined + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + } + } + + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } + + // Since it is possible the injected props could fail to satisfy the decoration props, + // the following line should fail to compile. + // @ts-expect-error + connect(mapStateToProps, mapDispatchToProps)(Component) + + // Confirm that this also fails with functional components + const FunctionalComponent = (props: MoreSpecificDecorationProps) => null + // @ts-expect-error + connect(mapStateToProps, mapDispatchToProps)(Component) +} + +function TestLibraryManagedAttributes() { + interface OwnProps { + bar: number + fn: () => void + } + + interface ExternalOwnProps { + bar?: number | undefined + fn: () => void + } + + interface MapStateProps { + foo: string + } + + class Component extends React.Component { + static defaultProps = { + bar: 0, + } + + render() { + return
+ } + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + } + } + + const ConnectedComponent = connect(mapStateToProps)(Component) + ; {}} /> + + const ConnectedComponent2 = connect( + mapStateToProps + )(Component) + ; {}} /> +} + +function TestPropTypes() { + interface OwnProps { + bar: number + fn: () => void + } + + interface MapStateProps { + foo: string + } + + class Component extends React.Component { + static propTypes = { + foo: PropTypes.string.isRequired, + bar: PropTypes.number.isRequired, + fn: PropTypes.func.isRequired, + } + + render() { + return
+ } + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + } + } + + const ConnectedComponent = connect(mapStateToProps)(Component) + ; {}} bar={0} /> + + const ConnectedComponent2 = connect( + mapStateToProps + )(Component) + ; {}} bar={0} /> +} + +function TestNonReactStatics() { + interface OwnProps { + bar: number + } + + interface MapStateProps { + foo: string + } + + class Component extends React.Component { + static defaultProps = { + bar: 0, + } + + static meaningOfLife = 42 + + render() { + return
+ } + } + + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + } + } + + Component.meaningOfLife + Component.defaultProps.bar + + const ConnectedComponent = connect(mapStateToProps)(Component) + + // This is a non-React static and should be hoisted as-is. + ConnectedComponent.meaningOfLife + + // This is a React static, so it's not hoisted. + // However, ConnectedComponent is still a ComponentClass, which specifies `defaultProps` + // as an optional static member. We can force an error (and assert that `defaultProps` + // wasn't hoisted) by reaching into the `defaultProps` object without a null check. + // @ts-expect-error + ConnectedComponent.defaultProps.bar +} + +function TestProviderContext() { + const store: Store = createStore((state = {}) => state) + const nullContext = React.createContext(null) + + // To ensure type safety when consuming the context in an app, a null-context does not suffice. + // @ts-expect-error + ; + ; +
+ + + // react-redux exports a default context used internally if none is supplied, used as shown below. + class ComponentWithDefaultContext extends React.Component { + static contextType = ReactReduxContext + } + + ; + + + + // Null is not a valid value for the context. + // @ts-expect-error + ; +} + +function testConnectedProps() { + interface OwnProps { + own: string + } + const Component: React.FC = ({ own, dispatch }) => null + + const connector = connect() + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) +} + +function testConnectedPropsWithState() { + interface OwnProps { + own: string + } + const Component: React.FC = ({ + own, + injected, + dispatch, + }) => { + injected.slice() + return null + } + + const connector = connect((state: any) => ({ injected: '' })) + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) +} + +function testConnectedPropsWithStateAndActions() { + interface OwnProps { + own: string + } + const actionCreator = () => ({ type: 'action' }) + + const Component: React.FC = ({ + own, + injected, + actionCreator, + }) => { + actionCreator() + return null + } + + const ComponentWithDispatch: React.FC = ({ + own, + // @ts-expect-error + dispatch, + }) => null + + const connector = connect((state: any) => ({ injected: '' }), { + actionCreator, + }) + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) +} + +function testConnectReturnType() { + const TestComponent: React.FC = () => null + + const Test = connect()(TestComponent) + + const myHoc1 = (C: React.ComponentClass

): React.ComponentType

=> C + // @ts-expect-error + myHoc1(Test) + + const myHoc2 = (C: React.FC

): React.ComponentType

=> C + // TODO Figure out the error here + // myHoc2(Test) +} + +function testRef() { + const FunctionalComponent: React.FC = () => null + const ForwardedFunctionalComponent = React.forwardRef(() => null) + class ClassComponent extends React.Component {} + + const ConnectedFunctionalComponent = connect()(FunctionalComponent) + const ConnectedForwardedFunctionalComponent = connect()( + ForwardedFunctionalComponent + ) + const ConnectedClassComponent = connect()(ClassComponent) + + // Should not be able to pass any type of ref to a FunctionalComponent + // ref is not a valid property + ;()} + > + ; {}} + > + + // @ts-expect-error + ; + + // Should be able to pass modern refs to a ForwardRefExoticComponent + const modernRef: React.Ref | undefined = undefined + ; + // Should not be able to use legacy string refs + ; + // ref type should agree with type of the forwarded ref + ;()} + > + ; {}} + > + + // Should be able to use all refs including legacy string + const classLegacyRef: React.LegacyRef | undefined = undefined + ; + ;()} + > + ; {}} + > + ; + // ref type should be the typeof the wrapped component + ;()} + > + // @ts-expect-error + ; {}}> +} + +function testConnectDefaultState() { + connect((state) => { + const s = state + expectType(s) + return state + }) + + const connectWithDefaultState: Connect<{ value: number }> = connect + connectWithDefaultState((state) => { + const s = state + expectType<{ value: number }>(state) + return state + }) +} + +function testPreserveDiscriminatedUnions() { + type OwnPropsT = { + color: string + } & ( + | { + type: 'plain' + } + | { + type: 'localized' + params: Record | undefined + } + ) + + class MyText extends React.Component {} + + const ConnectedMyText = connect()(MyText) + const someParams = { key: 'value', foo: 'bar' } + + ; + // @ts-expect-error + ; + // @ts-expect-error + ; + ; +} diff --git a/test/typetests/counterApp.ts b/test/typetests/counterApp.ts new file mode 100644 index 000000000..7b976d814 --- /dev/null +++ b/test/typetests/counterApp.ts @@ -0,0 +1,56 @@ +import { + createSlice, + createAsyncThunk, + configureStore, + ThunkAction, + Action, +} from '@reduxjs/toolkit' + +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 + } +) + +export const { increment } = counterSlice.actions + +const counterStore = configureStore({ + reducer: counterSlice.reducer, + middleware: (gdm) => gdm(), +}) + +export type AppDispatch = typeof counterStore.dispatch +export type RootState = ReturnType +export type AppThunk = ThunkAction< + ReturnType, + RootState, + unknown, + Action +> diff --git a/test/typetests/hooks.tsx b/test/typetests/hooks.tsx new file mode 100644 index 000000000..0548c57aa --- /dev/null +++ b/test/typetests/hooks.tsx @@ -0,0 +1,228 @@ +/* eslint-disable @typescript-eslint/no-unused-vars, no-inner-declarations */ + +import * as React from 'react' +import * as ReactDOM from 'react-dom' +import { Store, Dispatch, configureStore } from '@reduxjs/toolkit' +import { + connect, + ConnectedProps, + Provider, + DispatchProp, + MapStateToProps, + ReactReduxContext, + ReactReduxContextValue, + Selector, + shallowEqual, + MapDispatchToProps, + useDispatch, + useSelector, + useStore, + createDispatchHook, + createSelectorHook, + createStoreHook, + TypedUseSelectorHook, +} from '../../src/index' + +import { + CounterState, + counterSlice, + increment, + incrementAsync, + AppDispatch, + AppThunk, + RootState, + fetchCount, +} from './counterApp' + +function preTypedHooksSetup() { + // Standard hooks setup + const useAppDispatch = () => useDispatch() + const useAppSelector: TypedUseSelectorHook = useSelector + + function CounterComponent() { + const dispatch = useAppDispatch() + + return ( +