From 50c72d550177c7ef9c4317776c8443cf03c46afd Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 00:24:55 -0600 Subject: [PATCH 01/59] Rename `connect-mapstate-mapdispatch.tsx` to `connect-mapstate-mapdispatch.test-d.tsx` --- ...te-mapdispatch.tsx => connect-mapstate-mapdispatch.test-d.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/typetests/{connect-mapstate-mapdispatch.tsx => connect-mapstate-mapdispatch.test-d.tsx} (100%) diff --git a/test/typetests/connect-mapstate-mapdispatch.tsx b/test/typetests/connect-mapstate-mapdispatch.test-d.tsx similarity index 100% rename from test/typetests/connect-mapstate-mapdispatch.tsx rename to test/typetests/connect-mapstate-mapdispatch.test-d.tsx From beaaca57a94dc79711270244b2323583e7836392 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 00:25:40 -0600 Subject: [PATCH 02/59] Rename `connect-options-and-issues.tsx` to `connect-options-and-issues.test-d.tsx` --- ...tions-and-issues.tsx => connect-options-and-issues.test-d.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/typetests/{connect-options-and-issues.tsx => connect-options-and-issues.test-d.tsx} (100%) diff --git a/test/typetests/connect-options-and-issues.tsx b/test/typetests/connect-options-and-issues.test-d.tsx similarity index 100% rename from test/typetests/connect-options-and-issues.tsx rename to test/typetests/connect-options-and-issues.test-d.tsx From 3630e057d67b03e394ce23a881359ab41a14482a Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 00:26:22 -0600 Subject: [PATCH 03/59] Rename `hooks.tsx` to `hooks.test-d.tsx` --- test/typetests/{hooks.tsx => hooks.test-d.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/typetests/{hooks.tsx => hooks.test-d.tsx} (100%) diff --git a/test/typetests/hooks.tsx b/test/typetests/hooks.test-d.tsx similarity index 100% rename from test/typetests/hooks.tsx rename to test/typetests/hooks.test-d.tsx From 5e762561034fb0b1e8f3d019ca3b4110b609fcc2 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 00:26:54 -0600 Subject: [PATCH 04/59] Rename `provider.tsx` to `provider.test-d.tsx` --- test/typetests/{provider.tsx => provider.test-d.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/typetests/{provider.tsx => provider.test-d.tsx} (100%) diff --git a/test/typetests/provider.tsx b/test/typetests/provider.test-d.tsx similarity index 100% rename from test/typetests/provider.tsx rename to test/typetests/provider.test-d.tsx From 0b2c4613ed478d64fde39b349f6c967096cfec7d Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 00:28:33 -0600 Subject: [PATCH 05/59] Rename `react-redux-types.typetest.tsx` to `react-redux-types.test-d.tsx` --- ...eact-redux-types.typetest.tsx => react-redux-types.test-d.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/typetests/{react-redux-types.typetest.tsx => react-redux-types.test-d.tsx} (100%) diff --git a/test/typetests/react-redux-types.typetest.tsx b/test/typetests/react-redux-types.test-d.tsx similarity index 100% rename from test/typetests/react-redux-types.typetest.tsx rename to test/typetests/react-redux-types.test-d.tsx From fc46a7984a549376f56d15ffb82a55472470d577 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 02:38:28 -0600 Subject: [PATCH 06/59] MIgrate type tests for `connect-options-and-issues.test-d.tsx` to Vitest --- .../connect-options-and-issues.test-d.tsx | 1597 +++++++++-------- 1 file changed, 799 insertions(+), 798 deletions(-) diff --git a/test/typetests/connect-options-and-issues.test-d.tsx b/test/typetests/connect-options-and-issues.test-d.tsx index be23afc4b..aca4e9c93 100644 --- a/test/typetests/connect-options-and-issues.test-d.tsx +++ b/test/typetests/connect-options-and-issues.test-d.tsx @@ -11,8 +11,6 @@ import type { } from '../../src/index' import { Provider, ReactReduxContext, connect } 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 @@ -20,879 +18,882 @@ 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) +describe('type tests', () => { + test('MergedPropsInference', () => { + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/16021 + + 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 ConnectedWithOwn = connect( - undefined, - undefined, - () => ({ + const ConnectedWithOwnAndState = connect< + StateProps, + void, + OwnProps, + MergedProps + >(mapStateToProps, undefined, (stateProps: StateProps) => ({ merged: 'merged', - }), - )(MergedPropsComponent) - - const ConnectedWithInferredDispatch = connect( - mapStateToProps, - undefined, - (stateProps, dispatchProps, ownProps) => { - expectType>(dispatchProps) - }, - )(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} -

- ) + }))(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) + + const ConnectedWithInferredDispatch = connect( + mapStateToProps, + undefined, + (stateProps, dispatchProps, ownProps) => { + expectTypeOf(dispatchProps).toEqualTypeOf>() + }, + )(MergedPropsComponent) }) - ;
-} - -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} -

- ) + + test('issue #16652', () => { + 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 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', + + test('issue #15463', () => { + interface SpinnerProps { + showGlobalSpinner: boolean + } + + class SpinnerClass extends React.Component { + render() { + return
+ } + } + + const Spinner = connect((state: any) => { + return { showGlobalSpinner: true } + })(SpinnerClass) + + ; }) - React.createElement(ConnectedWithOwnPropsClass, { - own: 'string', - forwarded: 'string', + + test('remove injected and pass on rest', () => { + interface TProps { + showGlobalSpinner: boolean + foo: string + } + class SpinnerClass extends React.Component { + render() { + return
+ } + } + + const Spinner = connect((state: any) => { + return { showGlobalSpinner: true } + })(SpinnerClass) + + ; }) - // This should not compile, it is missing ForwardedProps - // @ts-expect-error - React.createElement(ConnectedWithOwnPropsClass, { own: 'string' }) - // @ts-expect-error - React.createElement(ConnectedWithOwnPropsStateless, { own: 'string' }) + test('controlled component without DispatchProp', () => { + 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 + } - // This should compile - React.createElement(ConnectedWithOwnPropsClass, { - own: 'string', - forwarded: 'string', + 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) }) - React.createElement(ConnectedWithOwnPropsStateless, { - own: 'string', - forwarded: 'string', + + test('dispatch to props as object', () => { + 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) + ;
}) - // 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: () => {}, - } - } - - const Connected = 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 - } + test('inferred functional component with explicit own props', () => { + 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} +

+ ) + }) + ;
+ }) - interface MapDispatchProps { - dependsOnDispatch: () => void - } + test('inferred functional component with implicit own props', () => { + interface Props { + title: string + extraText: string + onClick: () => 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 - } + const Header = connect( + ({ app: { title } }: { app: { title: string } }) => ({ + title, + }), + (dispatch) => ({ + onClick: () => dispatch({ type: 'test' }), + }), + )(({ title, extraText, onClick }: Props) => { + return ( +

+ {title} {extraText} +

+ ) + }) + ;
+ }) - class Component extends React.Component { - render() { - return
+ test('wrapped component', () => { + interface InnerProps { + name: string + } + const Inner: React.FunctionComponent = (props) => { + return

{props.name}

} - } - interface MapStateProps { - foo: string | number - bar: number | 'foo' - dependsOnDispatch?: (() => void) | undefined - } + const mapStateToProps = (state: any) => { + return { + name: 'Connected', + } + } + const Connected = connect(mapStateToProps)(Inner) - interface MapDispatchProps { - dependsOnDispatch?: (() => void) | undefined - } + // `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 mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - bar: 42, + test('without own props decorated inference', () => { + interface ForwardedProps { + forwarded: string } - } - function mapDispatchToProps(dispatch: any): MapDispatchProps { - return { - dependsOnDispatch: () => {}, + interface OwnProps { + own: string } - } - // 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) + interface StateProps { + state: string + } - // Confirm that this also fails with functional components - const FunctionalComponent = (props: MoreSpecificDecorationProps) => null - // @ts-expect-error - connect(mapStateToProps, mapDispatchToProps)(Component) -} + class WithoutOwnPropsComponentClass extends React.Component< + ForwardedProps & StateProps & DispatchProp + > { + render() { + return
+ } + } -function TestLibraryManagedAttributes() { - interface OwnProps { - bar: number - fn: () => void - } + const WithoutOwnPropsComponentStateless: React.FunctionComponent< + ForwardedProps & StateProps & DispatchProp + > = () =>
- interface ExternalOwnProps { - bar?: number | undefined - fn: () => void - } + function mapStateToProps4(state: any, ownProps: OwnProps): StateProps { + return { state: 'string' } + } - interface MapStateProps { - foo: 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' }) - class Component extends React.Component { - static defaultProps = { - bar: 0, + interface AllProps { + own: string + state: string } - render() { - return
+ class AllPropsComponent extends React.Component< + AllProps & DispatchProp + > { + render() { + return
+ } } - } - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', + type PickedOwnProps = Pick + type PickedStateProps = Pick + + const mapStateToPropsForPicked: MapStateToProps< + PickedStateProps, + PickedOwnProps, + {} + > = (state: any): PickedStateProps => { + return { state: 'string' } } - } + const ConnectedWithPickedOwnProps = connect(mapStateToPropsForPicked)( + AllPropsComponent, + ) + ; + }) - const ConnectedComponent = connect(mapStateToProps)(Component) - ; {}} /> + test('provider accepts store with custom action', () => { + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/25321#issuecomment-387659500 - const ConnectedComponent2 = connect( - mapStateToProps, - )(Component) - ; {}} /> -} + const reducer: Reducer< + { foo: number } | undefined, + { type: 'foo'; payload: number } + > = (state) => state -function TestPropTypes() { - interface OwnProps { - bar: number - fn: () => void - } + const store = createStore(reducer) - interface MapStateProps { - foo: string - } + const Whatever = () => ( + +
Whatever
+
+ ) + }) - class Component extends React.Component { - static propTypes = { - foo: PropTypes.string.isRequired, - bar: PropTypes.number.isRequired, - fn: PropTypes.func.isRequired, + test('optional props merged correctly', () => { + interface OptionalDecorationProps { + foo: string + bar: number + optionalProp?: boolean | undefined + dependsOnDispatch?: (() => void) | undefined } - render() { - return
+ class Component extends React.Component { + render() { + return
+ } } - } - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', + interface MapStateProps { + foo: string + bar: number + optionalProp: boolean } - } - const ConnectedComponent = connect(mapStateToProps)(Component) - ; {}} bar={0} /> + interface MapDispatchProps { + dependsOnDispatch: () => void + } - const ConnectedComponent2 = connect( - mapStateToProps, - )(Component) - ; {}} bar={0} /> -} + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + optionalProp: true, + } + } -function TestNonReactStatics() { - interface OwnProps { - bar: number - } + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } - interface MapStateProps { - foo: string - } + const Connected = connect(mapStateToProps, mapDispatchToProps)(Component) + }) - class Component extends React.Component { - static defaultProps = { - bar: 0, - } - - static meaningOfLife = 42 - - render() { - return
- } - } - - function mapStateToProps(state: any): MapStateProps { - return { - foo: 'foo', - } - } + test('more general decoration props', () => { + // 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 + } - Component.meaningOfLife - Component.defaultProps.bar - - const ConnectedComponent = connect(mapStateToProps)(Component) + class Component extends React.Component { + render() { + return
+ } + } - // This is a non-React static and should be hoisted as-is. - ConnectedComponent.meaningOfLife + interface MapStateProps { + foo: string + bar: number + optionalProp: boolean + } - // 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 -} + interface MapDispatchProps { + dependsOnDispatch: () => void + } -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 - } - - // eslint-disable-next-line no-extra-semi - ; - - - - // 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 + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + optionalProp: true, + } + } - const connector = connect((state: any) => ({ injected: '' }), { - actionCreator, - }) - type ReduxProps = ConnectedProps + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } - const ConnectedComponent = connect(Component) -} + connect(mapStateToProps, mapDispatchToProps)(Component) + }) -function testConnectReturnType() { - const TestComponent: React.FC = () => null + test('fails more specific injected props', () => { + interface MoreSpecificDecorationProps { + foo: string + bar: number + dependsOnDispatch: () => void + } - const Test = connect()(TestComponent) + class Component extends React.Component { + render() { + return
+ } + } - const myHoc1 = (C: React.ComponentClass

): React.ComponentType

=> C - // @ts-expect-error - myHoc1(Test) + interface MapStateProps { + foo: string | number + bar: number | 'foo' + dependsOnDispatch?: (() => void) | undefined + } - const myHoc2 = (C: React.FC

): React.ComponentType

=> C - // TODO Figure out the error here - // myHoc2(Test) -} + interface MapDispatchProps { + dependsOnDispatch?: (() => void) | undefined + } -function testRef() { - const FunctionalComponent: React.FC = () => null - const ForwardedFunctionalComponent = React.forwardRef(() => null) - class ClassComponent extends React.Component {} + function mapStateToProps(state: any): MapStateProps { + return { + foo: 'foo', + bar: 42, + } + } - const ConnectedFunctionalComponent = connect()(FunctionalComponent) - const ConnectedForwardedFunctionalComponent = connect()( - ForwardedFunctionalComponent, - ) - const ConnectedClassComponent = connect()(ClassComponent) + function mapDispatchToProps(dispatch: any): MapDispatchProps { + return { + dependsOnDispatch: () => {}, + } + } - // 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 - ; null // @ts-expect-error - ref={''} - > - // ref type should agree with type of the forwarded ref - ; { + 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) + ; {}} /> + }) + + test('PropTypes', () => { + 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} /> + }) + + test('non react statics', () => { + 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 - ref={React.createRef()} - > - ; { + 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 - ref={(ref: number) => {}} - > - - // 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 - ; + ; +
+ + + // react-redux exports a default context used internally if none is supplied, used as shown below. + class ComponentWithDefaultContext extends React.Component { + static contextType = ReactReduxContext + } + + // eslint-disable-next-line no-extra-semi + ; + + + + // Null is not a valid value for the context. // @ts-expect-error - ref={React.createRef()} - > - // @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 + test('connected props', () => { + interface OwnProps { + own: string + } + const Component: React.FC = ({ own, dispatch }) => + null + + const connector = connect() + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) }) -} - -function testPreserveDiscriminatedUnions() { - type OwnPropsT = { - color: string - } & ( - | { - type: 'plain' - } - | { - type: 'localized' - params: Record | undefined - } - ) - class MyText extends React.Component {} + test('connected props with state', () => { + interface OwnProps { + own: string + } + const Component: React.FC = ({ + own, + injected, + dispatch, + }) => { + injected.slice() + return null + } - const ConnectedMyText = connect()(MyText) - const someParams = { key: 'value', foo: 'bar' } + const connector = connect((state: any) => ({ injected: '' })) + type ReduxProps = ConnectedProps - ; - // @ts-expect-error - ; - // @ts-expect-error - ; - ; -} + const ConnectedComponent = connect(Component) + }) -function issue1187ConnectAcceptsPropNamedContext() { - const mapStateToProps = (state: { name: string }) => { - return { - name: state.name, + test('connected props with state and actions', () => { + interface OwnProps { + own: string + } + const actionCreator = () => ({ type: 'action' }) + + const Component: React.FC = ({ + own, + injected, + actionCreator, + }) => { + actionCreator() + return null } - } - const connector = connect(mapStateToProps) + const ComponentWithDispatch: React.FC = ({ + own, + // @ts-expect-error + dispatch, + }) => null + + const connector = connect((state: any) => ({ injected: '' }), { + actionCreator, + }) + type ReduxProps = ConnectedProps + + const ConnectedComponent = connect(Component) + }) + + test('connect return type', () => { + 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) + }) + + test('Ref', () => { + 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 + ; + ;()} /> + ; {}} /> + ; + ;()} + /> + ; {}} + /> + }) + + test('connect default state', () => { + connect((state) => { + const s = state - type PropsFromRedux = ConnectedProps + expectTypeOf(s).toBeUnknown() - interface IButtonOwnProps { - label: string - context: 'LIST' | 'CARD' - } - type IButtonProps = IButtonOwnProps & PropsFromRedux + return state + }) - function Button(props: IButtonProps) { - const { name, label, context } = props - return ( - + const connectWithDefaultState: Connect<{ value: number }> = connect + connectWithDefaultState((state) => { + expectTypeOf(state).toEqualTypeOf<{ value: number }>() + + return state + }) + }) + + test('preserve discriminated unions', () => { + type OwnPropsT = { + color: string + } & ( + | { + type: 'plain' + } + | { + type: 'localized' + params: Record | undefined + } ) - } - const ConnectedButton = connector(Button) + class MyText extends React.Component {} + + const ConnectedMyText = connect()(MyText) + const someParams = { key: 'value', foo: 'bar' } - // Since `IButtonOwnProps` includes a field named `context`, the final - // connected component _should_ use exactly that type, and omit the - // built-in `context: ReactReduxContext` field definition. - // If the types are broken, then `context` will have an error like: - // Type '"LIST"' is not assignable to type '("LIST" | "CARD") & (Context> | undefined)' - return -} + ; + // @ts-expect-error + ; + // @ts-expect-error + ; + ; + }) + + test('issue #1187 connect accepts prop named context', () => { + const mapStateToProps = (state: { name: string }) => { + return { + name: state.name, + } + } + + const connector = connect(mapStateToProps) + + type PropsFromRedux = ConnectedProps + + interface IButtonOwnProps { + label: string + context: 'LIST' | 'CARD' + } + type IButtonProps = IButtonOwnProps & PropsFromRedux + + function Button(props: IButtonProps) { + const { name, label, context } = props + return ( + + ) + } + + const ConnectedButton = connector(Button) + + // Since `IButtonOwnProps` includes a field named `context`, the final + // connected component _should_ use exactly that type, and omit the + // built-in `context: ReactReduxContext` field definition. + // If the types are broken, then `context` will have an error like: + // Type '"LIST"' is not assignable to type '("LIST" | "CARD") & (Context> | undefined)' + return + }) +}) From 8fe27c36f4c7af82cef8d1f861bf388a0d2287e5 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 02:45:44 -0600 Subject: [PATCH 07/59] Fix minor issues in some test files --- test/components/Provider.spec.tsx | 2 +- test/hooks/useReduxContext.spec.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/components/Provider.spec.tsx b/test/components/Provider.spec.tsx index 05b7704e9..a496845f8 100644 --- a/test/components/Provider.spec.tsx +++ b/test/components/Provider.spec.tsx @@ -169,7 +169,7 @@ describe('React', () => { action.type === 'INC' ? state + 1 : state const innerStore = createStore(reducer) - const innerMapStateToProps = vi.fn((state) => ({ + const innerMapStateToProps = vi.fn<[number], TStateProps>((state) => ({ count: state, })) class Inner extends Component { diff --git a/test/hooks/useReduxContext.spec.tsx b/test/hooks/useReduxContext.spec.tsx index c11740bd2..c064d915e 100644 --- a/test/hooks/useReduxContext.spec.tsx +++ b/test/hooks/useReduxContext.spec.tsx @@ -14,7 +14,7 @@ describe('React', () => { const { result } = renderHook(() => useReduxContext()) - expect(result.error.message).toMatch( + expect(result.error?.message).toMatch( /could not find react-redux context value/, ) @@ -29,7 +29,7 @@ describe('React', () => { const { result } = renderHook(() => useCustomReduxContext()) - expect(result.error.message).toMatch( + expect(result.error?.message).toMatch( /could not find react-redux context value/, ) From e2777b7822cc605df8cb5dcfb48b219d6cc26707 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 02:48:51 -0600 Subject: [PATCH 08/59] Extend from the root `tsconfig.json` --- test/tsconfig.test.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/tsconfig.test.json b/test/tsconfig.test.json index 461da76db..68cb73574 100644 --- a/test/tsconfig.test.json +++ b/test/tsconfig.test.json @@ -1,4 +1,5 @@ { + "extends": "../tsconfig.json", "compilerOptions": { "allowSyntheticDefaultImports": true, "esModuleInterop": true, @@ -13,6 +14,6 @@ "baseUrl": ".", "skipLibCheck": true, "noImplicitReturns": false, - "experimentalDecorators": true, + "experimentalDecorators": true } } From 2c79718ee8f3cef3c6a219390fe215619d071210 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 05:53:04 -0600 Subject: [PATCH 09/59] Fix `exclude` in `tsconfig.test.json` --- test/tsconfig.test.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/tsconfig.test.json b/test/tsconfig.test.json index 68cb73574..1b4917503 100644 --- a/test/tsconfig.test.json +++ b/test/tsconfig.test.json @@ -15,5 +15,6 @@ "skipLibCheck": true, "noImplicitReturns": false, "experimentalDecorators": true - } + }, + "exclude": [] } From 2957c5fe86e2b9f5f974464770e19c003cbb310a Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Mon, 29 Jan 2024 05:53:32 -0600 Subject: [PATCH 10/59] Convert functions to `test` blocks in `connect-mapstate-mapdispatch.test-d.tsx` --- .../connect-mapstate-mapdispatch.test-d.tsx | 808 +++++++++--------- 1 file changed, 407 insertions(+), 401 deletions(-) diff --git a/test/typetests/connect-mapstate-mapdispatch.test-d.tsx b/test/typetests/connect-mapstate-mapdispatch.test-d.tsx index 8f30cf355..a139ba173 100644 --- a/test/typetests/connect-mapstate-mapdispatch.test-d.tsx +++ b/test/typetests/connect-mapstate-mapdispatch.test-d.tsx @@ -12,482 +12,488 @@ const CustomContext = React.createContext( null, ) as unknown as typeof ReactReduxContext -function Empty() { - interface OwnProps { - dispatch: Dispatch - foo: string - } +describe('type tests', () => { + test('Empty', () => { + interface OwnProps { + dispatch: Dispatch + foo: string + } + + class TestComponent extends React.Component {} + + const Test = connect()(TestComponent) - class TestComponent extends React.Component {} + const verify = + }) - const Test = connect()(TestComponent) + test('MapState', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } - const verify = -} + class TestComponent extends React.Component {} -function MapState() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } + const mapStateToProps = (_: any) => ({ + bar: 1, + }) - class TestComponent extends React.Component {} + const Test = connect(mapStateToProps)(TestComponent) - const mapStateToProps = (_: any) => ({ - bar: 1, + const verify = }) - const Test = connect(mapStateToProps)(TestComponent) + test('MapStateWithDispatchProp', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + dispatch: Dispatch + } - const verify = -} + class TestComponent extends React.Component {} -function MapStateWithDispatchProp() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - dispatch: Dispatch - } + const mapStateToProps = (_: any) => ({ + bar: 1, + }) - class TestComponent extends React.Component {} + const Test = connect(mapStateToProps)(TestComponent) - const mapStateToProps = (_: any) => ({ - bar: 1, + const verify = }) - const Test = connect(mapStateToProps)(TestComponent) + test('MapStateFactory', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } - const verify = -} + class TestComponent extends React.Component {} -function MapStateFactory() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } + const mapStateToProps = () => () => ({ + bar: 1, + }) - class TestComponent extends React.Component {} + const Test = connect(mapStateToProps)(TestComponent) - const mapStateToProps = () => () => ({ - bar: 1, + const verify = }) - const Test = connect(mapStateToProps)(TestComponent) + test('MapDispatch', () => { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} - const verify = -} + const mapDispatchToProps = { onClick: () => {} } -function MapDispatch() { - interface OwnProps { - foo: string - } - interface DispatchProps { - onClick: () => void - } + const TestNull = connect(null, mapDispatchToProps)(TestComponent) - class TestComponent extends React.Component {} + const verifyNull = - const mapDispatchToProps = { onClick: () => {} } + const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) - const TestNull = connect(null, mapDispatchToProps)(TestComponent) + const verifyUndefined = + }) - const verifyNull = + test('MapDispatchUnion', () => { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } - const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) + class TestComponent extends React.Component {} - const verifyUndefined = -} + // 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 -function MapDispatchUnion() { - interface OwnProps { - foo: string - } - interface DispatchProps { - onClick: () => void - } + const TestNull = connect(null, mapDispatchToProps)(TestComponent) - class TestComponent extends React.Component {} + const verifyNull = - // 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 TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) - const TestNull = connect(null, mapDispatchToProps)(TestComponent) + const verifyUndefined = + }) - const verifyNull = + test('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 = ( +

+ ; + + ; + +
+ ) + }) - const TestUndefined = connect(undefined, mapDispatchToProps)(TestComponent) + test('MapManualDispatchThatLooksLikeThunk', () => { + interface OwnProps { + foo: string + } + interface TestComponentProps extends OwnProps { + remove: (item: string) => () => object + } + class TestComponent extends React.Component { + render() { + return
+ } + } - const verifyUndefined = -} + const mapStateToProps = ({ foo }: { foo: string }) => ({ foo }) + function mapDispatchToProps(dispatch: Dispatch) { + return { + remove(item: string) { + return () => dispatch({ type: 'REMOVE_ITEM', item }) + }, + } + } -function MapDispatchWithThunkActionCreators() { - const simpleAction = (payload: boolean) => ({ - type: 'SIMPLE_ACTION', - payload, + 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 = ( +
+ ; + + ; + +
+ ) }) - 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 }) - }, + + test('MapStateAndDispatchObject', () => { + interface ClickPayload { + count: number } - } - - 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, - }) + 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 MapStateAndNullishDispatch() { - interface ClickPayload { - count: number - } - const onClick: ActionCreator = () => ({ count: 1 }) - const dispatchToProps = { - onClick, - } - - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - - const mapStateToProps = (_: any, __: OwnProps): StateProps => ({ - bar: 1, + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} + + const Test = connect(mapStateToProps, dispatchToProps)(TestComponent) + + const verify = }) - class TestComponent extends React.Component {} + test('MapStateAndNullishDispatch', () => { + interface ClickPayload { + count: number + } + const onClick: ActionCreator = () => ({ count: 1 }) + const dispatchToProps = { + onClick, + } - const TestDispatchPropsNull = connect(mapStateToProps, null)(TestComponent) + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } - const verifyNull = + const mapStateToProps = (_: any, __: OwnProps): StateProps => ({ + bar: 1, + }) - const TestDispatchPropsUndefined = connect( - mapStateToProps, - undefined, - )(TestComponent) + class TestComponent extends React.Component {} - const verifyNonUn = -} + const TestDispatchPropsNull = connect(mapStateToProps, null)(TestComponent) -function MapDispatchFactory() { - interface OwnProps { - foo: string - } - interface DispatchProps { - onClick: () => void - } + const verifyNull = - class TestComponent extends React.Component {} + const TestDispatchPropsUndefined = connect( + mapStateToProps, + undefined, + )(TestComponent) - const mapDispatchToPropsFactory = () => () => ({ - onClick: () => {}, + const verifyNonUn = }) - const TestNull = connect(null, mapDispatchToPropsFactory)(TestComponent) + test('MapDispatchFactory', () => { + interface OwnProps { + foo: string + } + interface DispatchProps { + onClick: () => void + } - const verifyNull = + class TestComponent extends React.Component {} - const TestUndefined = connect( - undefined, - mapDispatchToPropsFactory, - )(TestComponent) + const mapDispatchToPropsFactory = () => () => ({ + onClick: () => {}, + }) - const verifyUndefined = -} + const TestNull = connect(null, mapDispatchToPropsFactory)(TestComponent) -function MapStateAndDispatch() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } + const verifyNull = - class TestComponent extends React.Component< - OwnProps & StateProps & DispatchProps - > {} + const TestUndefined = connect( + undefined, + mapDispatchToPropsFactory, + )(TestComponent) - const mapStateToProps = () => ({ - bar: 1, + const verifyUndefined = }) - const mapDispatchToProps = () => ({ - onClick: () => {}, - }) + test('MapStateAndDispatch', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } - const Test = connect(mapStateToProps, mapDispatchToProps)(TestComponent) + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} - const verify = -} + const mapStateToProps = () => ({ + bar: 1, + }) -function MapStateFactoryAndDispatch() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } + const mapDispatchToProps = () => ({ + onClick: () => {}, + }) - const mapStateToPropsFactory = () => () => ({ - bar: 1, - }) + const Test = connect(mapStateToProps, mapDispatchToProps)(TestComponent) - const mapDispatchToProps = () => ({ - onClick: () => {}, + const verify = }) - 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, - }) + test('MapStateFactoryAndDispatch', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } - const mapDispatchToPropsFactory = () => () => ({ - onClick: () => {}, - }) + const mapStateToPropsFactory = () => () => ({ + bar: 1, + }) - 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: () => {}, + }) + + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} - const mapDispatchToProps = () => ({ - onClick: () => {}, + const Test = connect( + mapStateToPropsFactory, + mapDispatchToProps, + )(TestComponent) + + const verify = }) - const mergeProps = ( - stateProps: StateProps, - dispatchProps: DispatchProps, - ) => ({ ...stateProps, ...dispatchProps }) - - const Test = connect( - mapStateToProps, - mapDispatchToProps, - mergeProps, - )(TestComponent) - - const verify = -} - -function MapStateAndMerge() { - interface OwnProps { - foo: string - } - interface StateProps { - bar: number - } - interface DispatchProps { - onClick: () => void - } - - class TestComponent extends React.Component {} - - const mapStateToProps = () => ({ - bar: 1, + test('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 = }) - const mergeProps = (stateProps: StateProps, _: null, ownProps: OwnProps) => ({ - ...stateProps, - ...ownProps, + test('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 = }) - const Test = connect(mapStateToProps, null, 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, + test('MapStateAndMerge', () => { + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + onClick: () => void + } + + class TestComponent extends React.Component {} + + const mapStateToProps = () => ({ + bar: 1, + }) + + const mergeProps = ( + stateProps: StateProps, + _: null, + ownProps: OwnProps, + ) => ({ + ...stateProps, + ...ownProps, + }) + + const Test = connect(mapStateToProps, null, mergeProps)(TestComponent) + + const verify = }) - const areStatePropsEqual = (next: StateProps, current: StateProps) => true + test('MapStateAndOptions', () => { + interface State { + state: string + } + interface OwnProps { + foo: string + } + interface StateProps { + bar: number + } + interface DispatchProps { + dispatch: Dispatch + } - const Test = connect( - mapStateToProps, - null, - null, - { - areStatePropsEqual, - }, - )(TestComponent) + class TestComponent extends React.Component< + OwnProps & StateProps & DispatchProps + > {} - const verify = -} + const mapStateToProps = (state: State) => ({ + bar: 1, + }) + + const areStatePropsEqual = (next: StateProps, current: StateProps) => true + + const Test = connect( + mapStateToProps, + null, + null, + { + areStatePropsEqual, + }, + )(TestComponent) + + const verify = + }) +}) From 1fc684bf5c488c6da2547c9fdbb9f195e0aabb57 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 8 Feb 2024 02:19:40 -0600 Subject: [PATCH 11/59] MIgrate type tests for `react-redux-types.test-d.tsx` to Vitest --- test/typetests/react-redux-types.test-d.tsx | 246 ++++++++++---------- 1 file changed, 124 insertions(+), 122 deletions(-) diff --git a/test/typetests/react-redux-types.test-d.tsx b/test/typetests/react-redux-types.test-d.tsx index 5ff820acb..c839bcaa5 100644 --- a/test/typetests/react-redux-types.test-d.tsx +++ b/test/typetests/react-redux-types.test-d.tsx @@ -10,8 +10,6 @@ import type { MapStateToProps, } from '../../src/index' import { Provider, connect } from '../../src/index' -import { expectType } from '../typeTestHelpers' - import type { CounterState } from './counterApp' import { increment } from './counterApp' @@ -239,8 +237,7 @@ class TestComponent extends Component {} const WrappedTestComponent = connect()(TestComponent) // return value of the connect()(TestComponent) is of the type TestComponent -let ATestComponent: React.ComponentType -ATestComponent = TestComponent +let ATestComponent: React.ComponentType = TestComponent ATestComponent = WrappedTestComponent let anElement: ReactElement @@ -277,154 +274,159 @@ if (container) { ) } -// stateless functions that uses mapStateToProps and mapDispatchToProps -function testStatelessFunctionWithMapArguments() { - interface GreetingProps { - name: string - onClick: () => void - } +describe('type tests', () => { + test('stateless functions that uses mapStateToProps and mapDispatchToProps', () => { + interface GreetingProps { + name: string + onClick: () => void + } - function Greeting(props: GreetingProps) { - return
Hello {props.name}
- } + function Greeting(props: GreetingProps) { + return
Hello {props.name}
+ } - const mapStateToProps = (state: any, ownProps: GreetingProps) => { - return { - name: 'Connected! ' + ownProps.name, + const mapStateToProps = (state: any, ownProps: GreetingProps) => { + return { + name: `Connected! ${ownProps.name}`, + } } - } - const mapDispatchToProps = ( - dispatch: Dispatch, - ownProps: GreetingProps, - ) => { - return { - onClick: () => { - dispatch({ type: 'GREETING', name: ownProps.name }) - }, + const mapDispatchToProps = ( + dispatch: Dispatch, + ownProps: GreetingProps, + ) => { + return { + onClick: () => { + dispatch({ type: 'GREETING', name: ownProps.name }) + }, + } } - } - const ConnectedGreeting = connect( - mapStateToProps, - mapDispatchToProps, - )(Greeting) -} + const ConnectedGreeting = connect( + mapStateToProps, + mapDispatchToProps, + )(Greeting) + }) -// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8787 -function testTOwnPropsInference() { - interface OwnProps { - own: string - } + test('OwnProps inference', () => { + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8787 - interface StateProps { - state: string - } + interface OwnProps { + own: string + } - class OwnPropsComponent extends React.Component { - render() { - return
+ interface StateProps { + state: string } - } - function mapStateToPropsWithoutOwnProps(state: any): StateProps { - return { state: 'string' } - } + class OwnPropsComponent extends React.Component { + render() { + return
+ } + } - function mapStateToPropsWithOwnProps( - state: any, - ownProps: OwnProps, - ): StateProps { - return { state: 'string' } - } + function mapStateToPropsWithoutOwnProps(state: any): StateProps { + return { state: 'string' } + } - const ConnectedWithoutOwnProps = connect(mapStateToPropsWithoutOwnProps)( - OwnPropsComponent, - ) - const ConnectedWithOwnProps = connect(mapStateToPropsWithOwnProps)( - OwnPropsComponent, - ) - const ConnectedWithTypeHint = connect( - mapStateToPropsWithoutOwnProps, - )(OwnPropsComponent) + function mapStateToPropsWithOwnProps( + state: any, + ownProps: OwnProps, + ): StateProps { + return { state: 'string' } + } - // @ts-expect-error - React.createElement(ConnectedWithoutOwnProps, { anything: 'goes!' }) + const ConnectedWithoutOwnProps = connect(mapStateToPropsWithoutOwnProps)( + OwnPropsComponent, + ) + const ConnectedWithOwnProps = connect(mapStateToPropsWithOwnProps)( + OwnPropsComponent, + ) + const ConnectedWithTypeHint = connect( + mapStateToPropsWithoutOwnProps, + )(OwnPropsComponent) - // This compiles, as expected. - React.createElement(ConnectedWithOwnProps, { own: 'string' }) + // @ts-expect-error + React.createElement(ConnectedWithoutOwnProps, { anything: 'goes!' }) - // This should not compile, which is good. - // @ts-expect-error - React.createElement(ConnectedWithOwnProps, { missingOwn: true }) + // This compiles, as expected. + React.createElement(ConnectedWithOwnProps, { own: 'string' }) - // This compiles, as expected. - React.createElement(ConnectedWithTypeHint, { own: 'string' }) + // This should not compile, which is good. + // @ts-expect-error + React.createElement(ConnectedWithOwnProps, { missingOwn: true }) - // This should not compile, which is good. - // @ts-expect-error - React.createElement(ConnectedWithTypeHint, { missingOwn: true }) + // This compiles, as expected. + React.createElement(ConnectedWithTypeHint, { own: 'string' }) - interface AllProps { - own: string - state: string - } + // This should not compile, which is good. + // @ts-expect-error + React.createElement(ConnectedWithTypeHint, { missingOwn: true }) - class AllPropsComponent extends React.Component { - render() { - return
+ interface AllProps { + own: string + state: string } - } - - type PickedOwnProps = Pick - type PickedStateProps = Pick - const mapStateToPropsForPicked: MapStateToProps< - PickedStateProps, - PickedOwnProps, - {} - > = (state: any): PickedStateProps => { - return { state: 'string' } - } - const ConnectedWithPickedOwnProps = connect(mapStateToPropsForPicked)( - AllPropsComponent, - ) - ; -} + class AllPropsComponent extends React.Component { + render() { + return
+ } + } -function connectedPropsTest() { - interface RootState { - isOn: boolean - } + type PickedOwnProps = Pick + type PickedStateProps = Pick - const mapState1 = (state: RootState) => ({ - isOn: state.isOn, + const mapStateToPropsForPicked: MapStateToProps< + PickedStateProps, + PickedOwnProps, + {} + > = (state: any): PickedStateProps => { + return { state: 'string' } + } + const ConnectedWithPickedOwnProps = connect(mapStateToPropsForPicked)( + AllPropsComponent, + ) + ; }) - const mapDispatch1 = { - toggleOn: () => ({ type: 'TOGGLE_IS_ON' }), - } + test('connected props', () => { + interface RootState { + isOn: boolean + } - const connector1 = connect(mapState1, mapDispatch1) + const mapState1 = (state: RootState) => ({ + isOn: state.isOn, + }) - // The inferred type will look like: - // {isOn: boolean, toggleOn: () => void} - type PropsFromRedux1 = ConnectedProps + const mapDispatch1 = { + toggleOn: () => ({ type: 'TOGGLE_IS_ON' }), + } - expectType<{ isOn: boolean; toggleOn: () => void }>({} as PropsFromRedux1) + const connector1 = connect(mapState1, mapDispatch1) - const exampleThunk = (id: number) => async (dispatch: Dispatch) => { - return 'test' - } + // The inferred type will look like: + // {isOn: boolean, toggleOn: () => void} + type PropsFromRedux1 = ConnectedProps - const mapDispatch2 = { exampleThunk } + assertType<{ + isOn: boolean + toggleOn: () => void + }>({} as PropsFromRedux1) - // Connect should "resolve thunks", so that instead of typing the return value of the - // prop as the thunk function, it dives down and uses the return value of the thunk function itself - const connector2 = connect(null, mapDispatch2) - type PropsFromRedux2 = ConnectedProps + const exampleThunk = (id: number) => async (dispatch: Dispatch) => { + return 'test' + } - expectType<{ exampleThunk: (id: number) => Promise }>( - {} as PropsFromRedux2, - ) -} + const mapDispatch2 = { exampleThunk } + + // Connect should "resolve thunks", so that instead of typing the return value of the + // prop as the thunk function, it dives down and uses the return value of the thunk function itself + const connector2 = connect(null, mapDispatch2) + type PropsFromRedux2 = ConnectedProps + + expectTypeOf<{ + exampleThunk: (id: number) => Promise + }>().toEqualTypeOf() + }) +}) From 8e4e87dee09fa6baed16eac2d069ec7e3240ff9e Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 8 Feb 2024 03:12:59 -0600 Subject: [PATCH 12/59] MIgrate type tests for `hooks.test-d.tsx` to Vitest --- src/hooks/useSelector.ts | 2 +- test/typetests/hooks.test-d.tsx | 441 +++++++++++++++++--------------- 2 files changed, 239 insertions(+), 204 deletions(-) diff --git a/src/hooks/useSelector.ts b/src/hooks/useSelector.ts index 365f48d9c..c7ced64f5 100644 --- a/src/hooks/useSelector.ts +++ b/src/hooks/useSelector.ts @@ -142,7 +142,7 @@ export function createSelectorHook( ? useDefaultReduxContext : createReduxContextHook(context) - const useSelector = ( + const useSelector = ( selector: (state: TState) => Selected, equalityFnOrOptions: | EqualityFn> diff --git a/test/typetests/hooks.test-d.tsx b/test/typetests/hooks.test-d.tsx index 5ae3af047..a126bcc42 100644 --- a/test/typetests/hooks.test-d.tsx +++ b/test/typetests/hooks.test-d.tsx @@ -1,12 +1,11 @@ -/* eslint-disable @typescript-eslint/no-unused-vars, no-inner-declarations */ - import type { AnyAction, Dispatch, Store } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit' -import * as React from 'react' +import { createContext } from 'react' import type { ReactReduxContextValue, Selector, TypedUseSelectorHook, + UseSelector, } from '../../src/index' import { createDispatchHook, @@ -17,226 +16,262 @@ import { useSelector, useStore, } from '../../src/index' - import type { AppDispatch, RootState } from './counterApp' import { incrementAsync } from './counterApp' -import { expectExactType, expectType } from '../typeTestHelpers' +describe('type tests', () => { + test('pre-typed hooks setup', () => { + // Standard hooks setup + const useAppDispatch = () => useDispatch() + + const useAppSelector: TypedUseSelectorHook = useSelector + + function CounterComponent() { + const dispatch = useAppDispatch() + + return ( +