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