1

Here's a short sample of what I'm trying to do:

import { connect } from "react-redux";

interface ErrorProps {
  error: true;
  description: string;
}

interface NoErrorProps {
  error: false;
}

type TestProps = ErrorProps | NoErrorProps;

function Test(props: TestProps) {
  return props.error ? <>Error: ${props.description}</> : <>OK</>;
}

// This is a test implementation that always returns no error
connect(() => ({ error: false }))(Test);

However, I get an error:

Argument of type '(props: TestProps) => Element' is not assignable to parameter of type 'ComponentType<Matching<{ error: boolean; } & DispatchProp<AnyAction>, TestProps>>'.
  Type '(props: TestProps) => Element' is not assignable to type 'FunctionComponent<Matching<{ error: boolean; } & DispatchProp<AnyAction>, TestProps>>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'PropsWithChildren<Matching<{ error: boolean; } & DispatchProp<AnyAction>, TestProps>>' is not assignable to type 'TestProps'.
        Type 'Matching<{ error: boolean; } & DispatchProp<AnyAction>, NoErrorProps> & { children?: ReactNode; }' is not assignable to type 'TestProps'.
          Type 'Matching<{ error: boolean; } & DispatchProp<AnyAction>, NoErrorProps> & { children?: ReactNode; }' is not assignable to type 'NoErrorProps'.
            Types of property 'error' are incompatible.
              Type 'boolean' is not assignable to type 'false'.

Now, I have used algebraic data types for a long time and usually am pretty confident that I understand what I'm doing. But here I'll explain my reasoning in (probably) painfully obvious detail because I'm stumped and suspect that I misunderstand something very basic and fundamental.

I'm trying to decipher this error message, and while I'm not entirely sure what's happening on the left part of the type comparison (with Redux types), it seems that every line is a reason for the line before it. So, I look at these two lines, disregarding the types on the left:

   Type '...' is not assignable to type 'TestProps'.
     Type '...' is not assignable to type 'NoErrorProps'.

And I notice that this means that whatever '...' means it does not change, so here I disregard it. But this sequence is not assignable to TestProps because it is not assignable to 'NoErrorProps'. Which means for something to be assignable TestProps, it must be assignable to NoErrorProps.

Which doesn't make any sense. I just checked that I'm still sane:

const _1: TestProps = { error: false };
const _2: TestProps = { error: true, description: "foo" };

First variable is not assignable to ErrorProps, second is not assignable to NoErrorProps, but both are perfectly assignable to TestProps, because that's how union types work. Why do the lines in the error message above imply the opposite?

Max Yankov
  • 12,551
  • 12
  • 67
  • 135
  • 1
    While this isn't a direct answer to your question: we _strongly_ recommend using the React-Redux hooks API instead of `connect` today, and one of the primary reasons for this is that the TS types are _drastically_ simpler to work with. Please consider using the hooks instead. – markerikson Feb 11 '22 at 22:11
  • It's a really weird and roundabout thing so I don't know if I can explain it eloquently. There are sometimes issues in TypeScript where a union is not assignable to itself because the union type `TestProps` is not assignable to any individual member of the union (`ErrorProps` or `NoErrorProps`). This is not an error `connect((): NoErrorProps => ({ error: false }))(Test);` but this is `connect((): TestProps => ({ error: false }))(Test);` – Linda Paiste Feb 12 '22 at 04:59
  • The types for the `connect` HOC are really complex and rely on a lot of backwards inference. For example, that `Matching` type which shows up in your error message is `type Matching = { [P in keyof DecorationTargetProps]: P extends keyof InjectedProps ? InjectedProps[P] extends DecorationTargetProps[P] ? DecorationTargetProps[P] : InjectedProps[P] : DecorationTargetProps[P]; }`. And that's just one piece of it! – Linda Paiste Feb 12 '22 at 05:01
  • @markerikson thanks for the recommendation, I'll bear this in mind! But in this instance, I'm working on an already quite large project and refactoring it throughout is quite a project – Max Yankov Feb 12 '22 at 10:42

1 Answers1

0

I might be mistaken, but I think it is all about contravariance.

TS compiler complains that (props: TestProps) => Element is not assignable to (props: {error:boolean}) => Element.

More simple example:

type UnionFn = (props: TestProps) => Element;
type SingleFn = (props: { error: boolean }) => Element;

declare let unionFn: UnionFn;
declare let singleFn: SingleFn;

unionFn = singleFn; // ok
singleFn = unionFn; // error

It might be strange, because TestProps is assignable to {error: boolean}:

declare let union: Union;
declare let single: Single;

union = single // expected error
single = union; // ok

However, when these types/values are in argument position, the arrow of inheritance turns into opposite way because arguments are in contravariant position. It means that singleFn is assignable to unionFn whereas unionFn is no more assignable to singleFn.

Hence, I think the issue is not in connect function typings.

More about *-variance you can find here

Here you can find my answer with more examples