reactjstypescripttypescript-generics

How to handle conditional type generics in Typescript along side React


I would like to find out if anyone has any good solutions to the below situation. I am trying to specifically have an optional property on my component dependent upon the type passed in. If the data type is NOT of a particular shape, then the property becomes required. Please see the example below :)

type TestProps<T> = ({
    data: T,
    onChange: (value: T) => void,
} & (T extends { label: string, value: string } ? { getValues?: (val: T) => number } : { getValues: (val: T) => string }));

function MyComponent<T>(props: TestProps<T>) {
    return <div></div>;
}

function Wrapper<T>(rest: TestProps<T>) {
    //Test 1: Should not display an error
    return <MyComponent {...rest} />;
}

type HaloObj = {
    code: string,
    name: string,
}

function TestImpl() {
    const normalObj = { label: 'label', value: 'value' };
    const customObj: HaloObj = { code: 'HAL', name: 'Halo Trilogy' };
    return (
        <>
            {/* Test 2: Should allow omitting the prop with no errors*/}
            <MyComponent
                data={normalObj}
                onChange={(value) => value}
            // getValues={(val) => 1}
            ></MyComponent>
            {/* Test 3: No Errors */}
            <MyComponent
                data={normalObj}
                onChange={(value) => value}
                getValues={(val) => 1}
            ></MyComponent>
            {/* Test 4: Should display an error required prop missing */}
            <MyComponent
                data={customObj}
                onChange={(value) => value}
            // getValues={(val) => val.code + val.name + "Hello World"}
            ></MyComponent>
            {/* Test 5: No errors, getValues typed properly*/}
            <MyComponent
                data={customObj}
                onChange={(value) => value}
                getValues={(val) => val.code + val.name + "Hello World"}
            ></MyComponent>
        </>
    )
}

The error received near Test 1, when wrapping the component, is:

Type 'TestProps<T>' is not assignable to type 'IntrinsicAttributes'.ts(2322)

Althought the wrapper doesn't work the TestImpl function, and Test 2,3,4,5 all work correctly. The typing for required/optional props works as expected.

I have tried adding [] around the T in line 4.

type TestProps<T> = ({
    data: T,
    onChange: (value: T) => void,
} & ([T] extends { label: string, value: string } ? { getValues?: (val: T) => number } : { getValues: (val: T) => string }));

This achieves the success in removing the error when wrapping the component. However, it then causes some of the other tests to fail specifically:

Test 2 with error:

Property 'getValues' is missing in type '{ data: { label: string; value: string; }; onChange: (value: { label: string; value: string; }) => { label: string; value: string; }; }' but required in type '{ getValues: (val: { label: string; value: string; }) => string; }'.ts(2741)
test.tsx(4, 92): 'getValues' is declared here.

Test 3 with error:

Type 'number' is not assignable to type 'string'.ts(2322)
test.tsx(4, 103): The expected type comes from the return type of this signature.

It seems to me like the square brace (some part of Distributive Conditional Types) Only ends in the second 'getValues' type being used. Any help would be appreciated :)


Solution

  • Type 'TestProps<T>' is not assignable to type 'IntrinsicAttributes &
      { data: T; onChange: (value: T) => void; } & (T extends { label: string; value: string; }
        ? { getValues?: ((val: T) => number) | undefined; } : { ...; })'.
    Type 'TestProps<T>' is not assignable to type 'IntrinsicAttributes'.typescript(2322)
    

    incorrect type

    This is saying that IntrinsicAttributes is missing from TestProps<T>. Note the IntrinsicAttributes & and then the rest of your TestProps<T> type definition.

    IntrinsicAttributes is the base of JSX props React components use.

    interface IntrinsicAttributes extends React.Attributes {}
    
    interface Attributes {
      key?: Key | null | undefined;
    }
    

    Update the Wrapper component props to extend the IntrinsicAttributes interface.

    Example:

    type WrapperProps<T> = JSX.IntrinsicAttributes & TestProps<T>;
    
    function Wrapper<T>(rest: WrapperProps<T>) {
      return <MyComponent {...rest} />;
    }
    

    Test case 4 is now the only one displaying issue, correctly as expected.

    ide

    This can also be resolved by updating the Wrapper component interface to limit T to objects.

    function Wrapper<T extends {}>(rest: TestProps<T>) {
      //Test 1: Should not display an error
      return <MyComponent {...rest} />;
    }
    

    ide