Ion Bazan
Ion Bazan

Reputation: 838

Infer type depending on other field type

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

Answers (1)

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"
    }
];

Playground

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

Related Questions