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
/>
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)
/>
);
}