Home > database >  Inferring Types for rowData and cellData in Columns Array
Inferring Types for rowData and cellData in Columns Array

Time:12-30

I'm trying to create a type for the following data structure:

const columns = [
  {
    dataKey: 'field1',
    label: 'Field 1',
    render: ({ rowData, cellData }) => {
      return null;
    },
  },
  {
    label: 'Actions',
    render: ({ rowData }) => {
      return null;
    },
  },
]

I wan't to be able to type the columns with a generic type and infer the types of rowData and cellData in each object of the list. If the dataKey is not present in the object, the cellData should be undefined.

I was able to get it almost working, when I specify the dataKey, both rowData and cellData shows with the correct types. But without it, the rowData becomes any, but if I put any value that is not part of the correct type typescript will complain but it will not infer the correct type.

type ColumnDef<T, U> = {
  label: string;
  render: (args: { rowData: T; cellData: U }) => null;
};

type ColumnWithoutKey<T> = ColumnDef<T, undefined>;

type ColumnWithKey<T> = {
  [K in keyof T]: T[K] extends infer TK
    ? { dataKey: K } & ColumnDef<T, TK>
    : never;
}[keyof T];

type HasDataKey = {
  dataKey: string;
};

type Column<T, U = any> = U extends HasDataKey
  ? ColumnWithKey<T>
  : ColumnWithoutKey<T>;

type Columns<T> = Array<Column<T>>;

type Data = {
  field1: string;
  field2: number; // only work if I have two or more fields
};

const columns: Columns<Data> = [
  {
    dataKey: 'field1', // can autocomplete the dataKey values
    label: 'Field 1',
    // rowData type is Data and cellData is string, both are correct
    render: ({ rowData, cellData }) => {
      return null;
    },
  },
  {
    label: 'Actions',
    // both rowData and cellData types are inferred as any
    render: ({ rowData, cellData }) => {
      return null;
    },
  },
  {
    label: 'Actions',
    // will complain that field3 doesn't exist in the render type, but if I change it to field2, the type continues any
    render: ({ rowData: { field3 } }) => {
      return null;
    },
  },
];

Playground

CodePudding user response:

Simply add an explicit (optional) dataKey property in your ColumnWithoutKey type as well, with type never, so that Columns type becomes a proper discriminated union (of columns with dataKey column with undefined dataKey):

type ColumnWithoutKey<T> = {
  dataKey?: never; // Explicit undefined discriminant property
} & ColumnDef<T, undefined>;

And now it works as expected:

const columns: Columns<Data> = [
  {
    dataKey: 'field1', // can autocomplete the dataKey values
    label: 'Field 1',
    // rowData type is Data and cellData is string, both are correct
    render: ({
      rowData,
      //^? Data
      cellData
      //^? string
    }) => {
      return null;
    },
  },
  {
    label: 'Actions',
    render: ({ // Okay
      rowData,
      //^? Data
      cellData
      //^? undefined
    }) => {
      return null;
    },
  },
  {
    label: 'Actions 2',
    // will complain that field3 doesn't exist in the render type
    render: ({ rowData: { field3 } }) => { // Error: Property 'field3' does not exist on type 'Data'.
      return null;
    },
  },
  {
    label: 'Actions 3',
    render: ({ rowData: { field2 } }) => { // Okay
      //                    ^? number
      return null;
    },
  },
];

Works as well with a single Property data type:

const columns2: Columns<{
  field0: boolean;
}> = [
    {
      dataKey: 'field0',
      label: 'Field Zero',
      render: ({ // Okay
        rowData,
        //^? { field0: boolean; }
        cellData
        //^? boolean
      }) => null
    },
    {
      label: 'Row actions',
      render: ({ rowData: { field3 } }) => null // Error: Property 'field3' does not exist on type '{ field0: boolean; }'.
    },
    {
      label: 'Row actions 2',
      render: ({ rowData }) => null // Okay
      //         ^? { field0: boolean; }
    }
  ];

Playground Link


BTW, you can simplfy your ColumnWithKey and Column types, there is no need for conditional type in these cases:

type ColumnWithKey<T> = {
  [K in keyof T]: { // No need for conditional type and inference
    dataKey: K
  } & ColumnDef<T, T[K]>
}[keyof T];

type Column<T> = // No need  for conditional type, direct union
  | ColumnWithKey<T>
  | ColumnWithoutKey<T>;

Playground Link

  • Related