skip to Main Content

I get this error from TS and it doesn’t make any sense,
because my T generic is fully compatible with types A | B, because it extends from it! It’s is not true what the error says that you can instantiate it with a incompatible type.

type MyProps<T extends A | B> = {
   initial: T;
   onChange: (value: T) => void;
}

function MyComponent<T extends A | B>(props: MyProps<T>){

    const changeHandler = (value: T) => {
       ...
       props.onChange(value);
    };

    return <SomeOtherComponent onChange={changeHandler} ... />
}

The error is thrown forthe onChange property of SomeOtherComponent, but the type of onChange in that component is (value: A | B) => void, so it’s fully compatible with T.

What am I missing here?


a link to example in ts playground: https://www.typescriptlang.org/play?#code/MYGwhgzhAECCDeAoRBfZpIwEJNcgLgJ4AOAptALKEAKATgPbEQA8AKtKQB76kB2AJjFjQAPtCwA+aAF5oSaAoCWvRfkVgQALmisA3IgXR6vAMIALMLwDmpbQAoAbhoCutnQEoZUh-UX99aIgAZs68wGrGlIQm9AC2xMZ8+Gwc3HyCcKLiEnbEDEzaVHSMLKwS7riG0MDGEPjVFtakABKW-CCktDLQji5urJ7SUvIGVQp5JQB0xuaWNr0gru76VSj6owq0pPjOtLzQzADKcaQA8vhmnTHxibz1M4020vDAjy1tHbQo0AD0EnjBULhRSRY6xM4XK5xBK8JK5fIQbTwIymN72JyLNywMRYQbeXz8aAoCrIFBAA

3

Answers


  1. In this case T extends A | B, your change handler is accepting the value of type T, while in SomeOtherComponent‘s onChange method you have a function that accepts the value of type A | B

    Here we are narrowing down the scope T extends A | B but T can not be assigned to the A | B type. If you change your SomeOtherComponent like below it will work

    function SomeOtherComponent<T extends A | B>(props: { onChange: (value: T) => void }){
    
    }
    

    when your function definition is like this

    const changeHandler = (value: T) => {
       props.onChange(value);
    };
    

    what you are doing is passing this function definition to your SomeOtherComponent‘s A | B type which is like passing T -> A | B but there could be reverse way around A | B -> T.

    Login or Signup to reply.
  2. SomeOtherComponent expects an onChange property of type (value: A | B) => void; that is, it expects an onChange property that will accept any value of either type A or type B.

    You’re trying to give it an onChange property of type (value: T) => void, where T could be (for example) just A or just B; that is, you’re trying to give it an onChange property that may not accept certain values that it needs onChange to accept.

    The technical term that you can search for is contravariance; you can use a function with more-permissive parameter-types than is expected, but not one with more-restrictive parameter-types, because then you’re imposing requirements on the caller that it may not satisfy.

    Login or Signup to reply.
  3. As other answers have pointed out it’s wise to ensure you’re not narrowing the type, by keeping the T extends A | B all the way down.

    In instances like this (where I am deliberately creating a function to be used for the value of a known prop); I find that typing changeHandler upfront can be useful as you can expect that changeHandler will be of the correct type.

    If the prop is just being passed through and called, maybe I wouldn’t bother with redefining it, but I assume this has just been done for the purpose of the question.

    class A {
    
    }
    
    class B {
    
    }
    
    type MyOtherProps<T extends A | B> = {
       onChange: (value: T) => void;
    }
    
    function SomeOtherComponent<T extends A |B> (props: MyOtherProps<T> ) {
    
    }
    
    
    type MyProps<T extends A | B> = {
       initial: T;
       onChange: (value: T) => void;
    }
    
    function MyComponent<T extends A | B>(props: MyProps<T>){
    
        const changeHandler: MyOtherProps<T>["onChange"] = (value) => {
           props.onChange(value);
        };
    
        return <SomeOtherComponent onChange={changeHandler} />
    }
    
    
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search