Search code examples
typescripttypescript-genericsunion-types

Typescript - Union Type and generic types throw incompatibility error


I have an issue with generic types working with union types. In my case a function is defined using a union type and when I call this function with an element of one of the types in the union I get an error I have been trying to explain for the whole day.

I managed to summarize in the following lines of code :

interface IA {
  a: string;
}
interface IB {
  b: string;
}
type IAll = IA | IB;

interface IG<T> {
  prop: T;
  fct: (p: T) => void;
}

function fAll(p: IG<IAll>) {
  return;
}

const a: IG<IA> = {
  prop: {a: "aaa"},
  fct: (p) => { return; }
};

fAll(a);

The last line fAll(a); returns a typing error that I just can't explain:

Argument of type 'IG<IA>' is not assignable to parameter of type 'IG<IAll>'.
  Types of property 'fct' are incompatible.
    Type '(p: IA) => void' is not assignable to type '(p: IAll) => void'.
      Types of parameters 'p' and 'p' are incompatible.
        Type 'IAll' is not assignable to type 'IA'.
          Type 'IB' is not assignable to type 'IA'.ts(2345)

Why can't an element of IA be applied the type IAll while IAll is the union of IA and IB ? From my understanding if IA matches, checking for IB is irrelevant.

Writing fAll(a as IG<IAll>); fixes the issue but I don't understand why it should be necessary.

Thanks for your help !


Solution

  • The issue is that IG<IA> | IG<IB> is not the same as IG<IA | IB>. If you change your code to the first one it will work however you will not be able to call a function like that.

    interface IA {
      a: string;
    }
    interface IB {
      b: string;
    }
    type IAll = IA | IB;
    
    interface IG<T> {
      prop: T;
      fct: (p: T) => void;
    }
    
    function fAll(p: IG<IAll>) {
      // p.fct will allow `IA | IB` as parameters. This is not what you're looking for.
      p.fct({
        b: "string"
      })
      return;
    }
    
    const a: IG<IA> = {
      prop: {a: "aaa"},
      fct: (p) => {
        // This will log out `p.a`, but `p.a` doesn't have to be provided by `fAll` function (see above).
        console.log(p.a);
      }
    };
    
    fAll(a);
    
    function fAllFixed(p: IG<IA> | IG<IB>) {
      // Thats better!
      p.fct({
        b: "string"
      })
      return;
    }
    
    fAllFixed(a);
    

    Playground

    I had a similar question a while back, and got an amazingly detailed reply from jcalz (which I still don't fully understand...). I would encourage you to check that out: https://stackoverflow.com/a/58632009/5554464