Home > Net >  How to make property types in interface depend on each other?
How to make property types in interface depend on each other?

Time:12-22

I can't seem to figure out a dynamic way to have dataItem always be the same type as the value of the field property. In the following example dataItem is always string | Date | number instead of being the specific one depending on what the field property is.

interface TableColumn<D> {
  field: keyof D
  renderCell: (dataItem: D[keyof D]) => ReactNode
}

interface Data {
  foo: string
  bar: Date
  fiz: number
}

const data: Data = {
  foo: "my foo",
  bar: new Date(),
  fiz: 32
}

const tableColumn: TableColumn<Data> = {
  field: "foo",
  renderCell: (dataItem) => dataItem // here dataItem should be string only
}

const tableColumn1: TableColumn<Data> = {
  field: "bar",
  renderCell: (dataItem) => dataItem // here dataItem should be Date only
}

const tableColumn2: TableColumn<Data> = {
  field: "fiz",
  renderCell: (dataItem) => dataItem // here dataItem should be number only
}

CodePudding user response:

An interface wouldn't work this way; you could possibly make it more generic like TableColumn<D, K> where K is a particular key type in keyof D, but then you'd have to specify K everywhere.

Instead I would make TableColumn<D> a distributive object type as coined in microsoft/TypeScript#47109 that evaluates to a union of the individual types corresponding to each K in keyof T. The distributive object type is a mapped type which turns every property value K from keyof D into one holding the desired type corresponding to that property key, and which is then immediately indexed into with keyof D. Like this:

type TableColumn<D> = { [K in keyof D]-?:
  {
    field: K,
    renderCell: (dataItem: D[K]) => ReactNode
  }
}[keyof D]

Here we generate the mapped type { [K in keyof D]-?: { field: K, renderCell: (dataItem: D[K]) => ReactNode }}, which becomes for Data something like

{
  foo: { field: "foo"; renderCell: (dataItem: string) => ReactNode }
  bar: { field: "bar"; renderCell: (dataItem: Date) => ReactNode }
  fiz: { field: "fiz"; renderCell: (dataItem: number) => ReactNode }
}

Note that the mapping modifier -? just makes sure that any optional properties in D become required in the mapped type, since we don't want any undefineds floating around here.

Then we index into this via {...}[keyof D], which turns that mapped type into a union of its value types, ultimately producing:

type TCD = TableColumn<Data>
/* type TCD = {
    field: "foo";
    renderCell: (dataItem: string) => ReactNode;
} | {
    field: "bar";
    renderCell: (dataItem: Date) => ReactNode;
} | {
    field: "fiz";
    renderCell: (dataItem: number) => ReactNode;
} */

That's the union type you want; in fact, it's a discriminated union where field is the discriminant property. That lets the compiler infer the type of renderCell from the type of field, and thus gives you the desired contextual typing on renderCell's unannotated callback parameter:

const tableColumn: TableColumn<Data> = {
  field: "foo",
  renderCell: (dataItem) => dataItem
  // (parameter) dataItem: string
}

const tableColumn1: TableColumn<Data> = {
  field: "bar",
  renderCell: (dataItem) => dataItem
  // (parameter) dataItem: Date
}

const tableColumn2: TableColumn<Data> = {
  field: "fiz",
  renderCell: (dataItem) => dataItem
  // (parameter) dataItem: number
}

Playground link to code

  • Related