reactjstypescripttypescript-generics

TypeScript different generic in array of interface


I've a type with a generic to define some extra options for a function component.

type FCWithOptions<T> = React.FC<{options?: T}>;

Using this type I'm defining some components with their own extra properties

interface ComponentAOptions {
    option1: string;
    option2: number;
}

interface ComponentBOptions {
    prop1: boolean;
    prop2: Record<string, string>;
}

type ComponentAFC = FCWithOptions<ComponentAOptions>
type ComponentBFC = FCWithOptions<ComponentBOptions>

const ComponentA: ComponentAFC = ({options}) => {
    return <>Component A</>
}

const ComponentB: ComponentBFC = ({options}) => {
    return <>Component B</>
}

then what I need is to pass these components in a container component using an array of interfaces that contains the component to pass and the custom options of that component through a property.

interface ComponentValue<T>{
    component: FCWithOptions<T>;
    componentOptions?: T;
}
interface MainComponentProps<C>{
    components: ComponentValue<C>[]
}
function MainComponent<C>({}: MainComponentProps<C>) {
    return <>Main</>
}

What I should be able to do is something like this:

<MainComponent components={[
    {component: ComponentA, componentOptions: {option1: "", option2: 2}},
    {component: ComponentB, componentOptions: { prop1: true, props: {} }}
]} />

But it works only for the first element of the array, if I add other elements I get some typescript assertions

How could I achieve this? Thanks

Here a playground


Solution

  • If you have a generic type F<T> and need to represent an array of values of that type where each element of the array has a different choice of T, that is, if you need a tuple type like [F<T0>, F<T1>, F<T2>, ⋯ , F<TN>], then the best approach is probably to use a mapped tuple/array type over a tuple of just the T0, T1, etc types. That is, if you have the type type T = [T0, T1, T2, ⋯, TN] and then you can apply type MapF<T> = {[I in keyof T]: F<T[I]>} to it to get [F<T0>, F<T1>, F<T2>, ⋯ , F<TN>].

    That means we should change your code to look like this:

    interface MainComponentProps<C extends any[]> {
        components: [...{ [I in keyof C]: ComponentValue<C[I]> }]
    }
    
    function MainComponent<C extends any[]>({ }: MainComponentProps<C>) {
        return <>Main</>
    }
    

    (where we're using C in place of T and ComponentValue<⋯> in place of of F<⋯>.) The only difference is that I've wrapped the type of components with a variadic tuple type ([...⋯]) in order to hint to the compiler that we want it to infer a tuple and not an unordered array of arbitrary length when calling MainComponent. (This use of variadic tuple types is described in ms/TS#39094, "The type [...T], where T is an array-like type parameter, can conveniently be used to indicate a preference for inference of tuple types").

    Let's test it out:

    <MainComponent components={[
        { component: ComponentA, componentOptions: { option1: "", option2: 2 } },
        { component: ComponentB, componentOptions: { prop1: true, prop2: {} } }
    ]} />
    
    // MainComponentProps<[{ option1: string; option2: number; }, { prop1: boolean; prop2: {}; }]>
    

    Looks good. The type C is inferred as [{ option1: string; option2: number; }, { prop1: boolean; prop2: {}; }] and therefore the mapped type [...{ [I in keyof C]: ComponentValue<C[I]> }] matches the type of the components property.

    Playground link to code