Reputation: 838
I am implementing a custom table component where developer can customize the way certain cell data is rendered. To make it more foolproof, I am trying to make TS infer the type of render()
argument based on the User
property chosen in id
property to the type of that property.
interface Col<T extends Record<string, any>> {
id: (keyof T & string);
render: (data: T[this["id"]]) => string;
}
interface User {
name: string;
enabled: boolean;
}
const cols: Array<Col<User>> = [
{
id: "name",
render: (name: string) => name;
},
{
id: "enabled",
render: (enabled: boolean) => enabled ? "yes" : "no";
}
];
but following error is produced:
TS2322: Type '(name: string) => string' is not assignable to type '(data: string | boolean) => string'.
Types of parameters 'name' and 'data' are incompatible.
Type 'string | boolean' is not assignable to type 'string'.
Type 'boolean' is not assignable to type 'string'.
This is because the first argument of render()
is inferred as union string | boolean
(and more types if User
interface contains more). I was expecting T[this["id"]]
to return the type of actual property chosen in this.id
property but it seems to expand to an union of all possible choices instead.
Is there any way to make the type resolution dynamic without adding another generic parameter to Col
?
Upvotes: 2
Views: 1231
Reputation: 33041
In order to do that, you need to create a union of all allowed states:
interface User {
name: string;
enabled: boolean;
}
type Values<T> = T[keyof T]
type Union<T> = Values<{
[Prop in keyof T]: {
id: Prop,
render: (name: T[Prop]) => string
}
}>
type Result = Union<User>[]
const cols: Result = [
{
id: "name",
render: (name /** string */) => name
},
{
id: "enabled",
render: (enabled /** boolean */) => enabled ? "yes" : "no"
}
];
Union
- iterates through each User
property and creates an object:
{
id: Prop,
render: (value: User[Prop])=>string
}
This object is important for us. In order to obtain it, we should use Values
helper. It returns a union of all object values.
Upvotes: 3