typescript-go icon indicating copy to clipboard operation
typescript-go copied to clipboard

Issues with react and redux

Open lukeapage opened this issue 6 months ago • 1 comments

We have a test case for common patterns with react redux and there are 3 differences between ts-go and tsc.

I've narrowed it down to the test cases that fail and created a repro:

https://github.com/lukeapage/ts-repro-1

just npm i and npm run test

lukeapage avatar May 29 '25 19:05 lukeapage

link to test file: https://github.com/lukeapage/ts-repro-1/blob/main/test.tsx

lukeapage avatar May 29 '25 19:05 lukeapage

The errors (please include them in your issues!)

test.tsx:58:3 - error TS2578: Unused '@ts-expect-error' directive.

58   // @ts-expect-error detects that the wrong type is used in map state to props (when no return type in mapStateToProps)
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
test.tsx:77:15 - error TS2741: Property 'stringProp' is missing in type '{}' but required in type '{ key?: Key | null | undefined; ref?: Ref<ExampleComponent> | undefined; context?: Context<ReactReduxContextValue<any, UnknownAction> | null> | undefined; store?: Store<any, UnknownAction, unknown> | undefined; readonly stringProp: string; defaultBooleanProp?: boolean | undefined; }'.

77   const c2 = <ConnectedFn />;
                 ~~~~~~~~~~~

  test.tsx:14:3 - 'stringProp' is declared here.
    14   stringProp: string;
         ~~~~~~~~~~
test.tsx:103:15 - error TS2741: Property 'stringProp' is missing in type '{}' but required in type '{ key?: Key | null | undefined; ref?: Ref<ExampleComponent> | undefined; context?: Context<ReactReduxContextValue<any, UnknownAction> | null> | undefined; store?: Store<any, UnknownAction, unknown> | undefined; readonly stringProp: string; defaultBooleanProp?: boolean | undefined; }'.

103   const c2 = <ConnectedFn />;
                  ~~~~~~~~~~~

  test.tsx:14:3 - 'stringProp' is declared here.
    14   stringProp: string;
         ~~~~~~~~~~

Found 3 errors in the same file, starting at: test.tsx:58

jakebailey avatar Jun 04 '25 07:06 jakebailey

Yet another issue related to type ordering. The connect function has the following first overload (which is the one chosen):

<TStateProps = {}, no_dispatch = {}, TOwnProps = {}, State = DefaultState>(mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>): InferableComponentEnhancerWithProps<TStateProps & DispatchProp, TOwnProps>;
// ...
type MapStateToProps<TStateProps, TOwnProps, State> = (state: State, ownProps: TOwnProps) => TStateProps;
type MapStateToPropsFactory<TStateProps, TOwnProps, State> = (initialState: State, ownProps: TOwnProps) => MapStateToProps<TStateProps, TOwnProps, State>;
type MapStateToPropsParam<TStateProps, TOwnProps, State> = MapStateToPropsFactory<TStateProps, TOwnProps, State> | MapStateToProps<TStateProps, TOwnProps, State> | null | undefined;

The problem here is the MapStateToPropsParam union type which gets ordered differently by the old and new compilers. This means we infer to MapStateToPropsFactory and MapStateToProps in different orders and therefore get differently ordered candidates. And when we subsequently pick the "first" candidate, we get different outcomes.

You can trigger the error with the old compiler simply by reordering the union type:

type MapStateToPropsParam<TStateProps, TOwnProps, State> = MapStateToProps<TStateProps, TOwnProps, State> | MapStateToPropsFactory<TStateProps, TOwnProps, State> | null | undefined;

The reordering could also be triggered simply by code referencing MapToStateProps being checked before checking the connect call.

Ideally, the React declaration files would break the union parameter into separate overloads and list them in order of preference.

ahejlsberg avatar Jun 17 '25 17:06 ahejlsberg

Thanks for the analysis! I’ve raised an issue in the react-redux github.

lukeapage avatar Jun 17 '25 17:06 lukeapage