Home > other >  Typescript React - Inferring prop type from another generic prop
Typescript React - Inferring prop type from another generic prop

Time:09-10

I have the following type which represents an item store:

export type ItemStore<T> = {
  allById: {
    [id: string]: T,
  },
  activeById: {
    [id: string]: T,
  },
  all: T[],
  active: T[],
  fetching: boolean,
  cursor: string,
}

And I have a component that utilizes an item store, but is written such that the store type is generic and it should infer the item type from the store type. I need labelKey and valueKey to accept the keys of the item type.

type Props<S, I = S extends ItemStore<infer IT> ? IT : never> = {
  store: S,
  labelKey: keyof I,
  valueKey: keyof I,
}

function StoreItemList<S>(props: Props<S>): JSX.Element {
  ...
}

Example usage of this might be:

type UserItem = {
  id: number,
  name: string,
}

const userStore: UserStore<UserItem> = { ... }

<StoreItemList
  store={userStore}
  labelKey="id"     // This should only accept 'id' and 'name' (keys of UserItem)
  valueKey="name"   // This should only accept 'id' and 'name' (keys of UserItem)
/>

However, this doesn't work. In the above example, it's evaluating store as unknown. Where am I going wrong?


Edit: Turns out this was ultimately an issue with the styled-components package I'm using. For some reason wrapping the component styled(StoreItemList) messes up the types. Once I removed the styled wrapper, the types worked as expected...


Edit 2: Looks like this is a known issue with styled-components when using generic components: https://github.com/styled-components/styled-components/issues/1803

CodePudding user response:

I guess you want props.store to always be an ItemStore? In that case, it seems a bit convoluted having Props["store"] be a generic type S and then trying to constrain it by inferring the generic type IT of ItemStore<IT> that is supposed to be extended by S.

Instead, you could just have a single generic type representing the item, and make props.store be an ItemStore<item>:

type Props2<T> = { // Single generic type, no inference needed
    store: ItemStore<T>; // Force store to be a proper ItemStore
    labelKey: keyof T;
    valueKey: keyof T;
};

// Assignment of generics from component to props is straightforward, 
// no hidden generic type
function StoreItemList2<T>(props: Props2<T>) {
    // ...
}

With this, passing an incorrect store value type is detected:

<StoreItemList2
    store={userStore} // Error: Type '{ id: number; name: string; }[]' is missing the following properties from type 'ItemStore<{ id_typo: any; } & { name: any; }>': allById, activeById, all, active, and 2 more.
//  ~~~~~
    labelKey="id_typo"     // This should only accept 'id' and 'name' (keys of UserItem)
    valueKey="name"   // This should only accept 'id' and 'name' (keys of UserItem)
/>

And, as initially expected, incorrect label or value keys, which should be keys of the item type:

const userStore2: ItemStore<UserItem> = {
    allById: {
        //[id: string]: T,
    },
    activeById: {
        //[id: string]: T,
    },
    all: [],
    active: [],
    fetching: false,
    cursor: ""
};

// Item type (here UserItem) is correctly inferred automatically from type of userStore2
<StoreItemList2
    store={userStore2}
    labelKey="id_typo" // Error: Type '"id_typo"' is not assignable to type 'keyof UserItem'.
//  ~~~~~~~~
    valueKey="name" // Okay
/>

Playground Link

CodePudding user response:

Seems like you need to pass a second type argument


export type ItemStore<T> = {
    allById: {
        [id: string]: T;
    };
    activeById: {
        [id: string]: T;
    };
    all: T[];
    active: T[];
    fetching: boolean;
    cursor: string;
};

type Props<S, I = S extends ItemStore<infer IT> ? IT : never> = {
    store: S;
    labelKey: keyof I;
    valueKey: keyof I;
};

function StoreItemList<S, I>(props: Props<S, I>): JSX.Element {
    return <div></div>;
}

type UserItem = {
    id: number;
    name: string;
};


function Foo() {
    const userStore = [
        {
            id: 1,
            name: 'James'
        },
        {
            id: 2,
            name: 'Jack'
        }
    ];

    return (
        <StoreItemList<any, UserItem>
            store={userStore}
            labelKey="id" // This should only accept 'id' and 'name' (keys of UserItem)
            valueKey="name" // This should only accept 'id' and 'name' (keys of UserItem)
        />
    );
}
  • Related