I want to create a custom table. The prop in the cell and footer in createColumnHelper function should return the correct object key/type instead of any.
For example, I have a nested object progress in my DATA. Or if not an object, it also should return the correct type.
I attached screenshots of how I see in the IDE
Helper.ts
import { ReactElement } from "react";
export type Column<T> = {
header: string | (() => ReactElement);
accessor: (row: T) => any;
cell?:(prop: any) => ReactElement | string;
footer?: (prop: any) => ReactElement | string;
};
export const createColumnHelper = <T>() => {
return {
accessor: (
accessor: keyof T | ((row: T) => any),
column: {
header: string | (() => ReactElement);
cell?: (prop: any) => ReactElement | string;
footer?: (prop: any) => ReactElement | string;
}
) => {
return {
header: column.header,
accessor: (row: T) => (typeof accessor === "function" ? accessor(row) : row[accessor]),
cell: column.cell,
footer: column.footer,
} as Column<T>;
}
};
};
TableComponents.tsx
import React from "react";
import { createColumnHelper } from "./columnHelper";
type Person = {
firstName: string
lastName: string
age: number
visits: number
status: string
progress: {
ok?: number;
no?: string
}
}
const DATA: Person[] = [
{
firstName: 'tanner',
lastName: 'linsley',
age: 24,
visits: 100,
status: 'In Relationship',
progress: {
ok: 50
},
},
{
firstName: 'tandy',
lastName: 'miller',
age: 40,
visits: 40,
status: 'Single',
progress: {
no: 'bad'
},
},
]
const columnHelper = createColumnHelper<Person>();
const columns = [
columnHelper.accessor(
'firstName', {
header: 'First Name',
cell: val => val,
}),
columnHelper.accessor(
'progress', {
header: 'Progress',
cell: prop => prop.ok,
}),
columnHelper.accessor(row => row, {
header: () => <span>Age</span>,
cell: prop => <span>{prop.age}</span>,
}),
];
const TableComponent: React.FC = () => {
return (
<table>
<thead>
<tr>
{columns.map((column, index) => (
<th key={index}>
{typeof column.header === "function" ? column.header() : column.header}
</th>
))}
</tr>
</thead>
<tbody>
{DATA.map((row, rowIndex) => (
<tr key={rowIndex}>
{columns.map((column, colIndex) => (
<td key={colIndex}>
{column.cell ? column.cell(column.accessor(row)) : column.accessor(row)}
</td>
))}
</tr>
))}
</tbody>
<tfoot>
<tr>
{columns.map((column, index) => (
<td key={index}>
{column.footer ? column.footer({ column }) : null}
</td>
))}
</tr>
</tfoot>
</table>
);
};
export default TableComponent;
Since you care about the input type of cell
/footer
and the output type of accessor
, you should consider adding a generic type parameter to Column
for that type:
type Column<T, V> = {
header: string | (() => ReactElement);
accessor: (row: T) => V;
cell?: (prop: V) => ReactElement | string;
footer?: (prop: V) => ReactElement | string;
};
Then the value returned by createColumnHelper<T>()
should have an accessor
method that either accepts a value of type K extends keyof T
and produces a Column<T, T[K]>
, or it accepts a value of type (row: T) => V
for some V
and produces a Column<T, V>
. That looks like:
const createColumnHelper = <T,>() => {
// call signatures
function accessor<K extends keyof T>(
accessor: K, column: Omit<Column<T, T[K]>, "accessor">
): Column<T, T[K]>;
function accessor<V>(
accessor: (row: T) => V, column: Omit<Column<T, V>, "accessor">
): Column<T, V>;
// implementation
function accessor(
accessor: keyof T | ((row: T) => any), column: Omit<Column<T, any>, "accessor">
): Column<T, any> {
return {
header: column.header,
accessor: (row: T) => (typeof accessor === "function" ? accessor(row) : row[accessor]),
cell: column.cell,
footer: column.footer,
};
}
return { accessor };
};
Note that I'm using Omit
to easily describe a Column
with the accessor
property missing. It's not necessary, but saves some writing over {header: ⋯, cell?: ⋯, footer?: ⋯}
. This fixes the problem you were having:
const columns = [
columnHelper.accessor(
'firstName', {
header: 'First Name',
cell: val => val,
}),
columnHelper.accessor(
'progress', {
header: 'Progress',
cell: prop => String(prop.ok),
}),
columnHelper.accessor(row => row, {
header: () => <span>Age</span>,
cell: prop => <span>{prop.age}</span>,
}),
];
(and note that I had to fix a bug, since prop.ok
is not a string
or a ReactElement
.)
That answers the question as asked, but if you do that, you'll run into problems with column.cell ? column.cell(column.accessor(row)) : column.accessor(row)
and the like. The only reason why that used to work is because column.cell()
accepts any
. Now it doesn't. And TS can't verify this as valid because of the multiple utterances of column
of a union type. This is ms/TS#30581 more or less, and the fixes for it are well out of scope for this question.
The easiest middle ground is to have columnHelper.accessor(a, o)
require that the input look like Omit<Column<T, T[K]>, "accessor">
or Omit<Column<T, V>, "accessor">
, but which returns a Column<T, any>
:
const createColumnHelper = <T,>() => {
// call signatures
function accessor<K extends keyof T>(
accessor: K, column: Omit<Column<T, T[K]>, "accessor">
): Column<T, any>;
function accessor<V>(
accessor: (row: T) => V, column: Omit<Column<T, V>, "accessor">
): Column<T, any>;
// implementation
function accessor(
accessor: keyof T | ((row: T) => any), column: Omit<Column<T, any>, "accessor">
): Column<T, any> {
return {
header: column.header,
accessor: (row: T) => (typeof accessor === "function" ? accessor(row) : row[accessor]),
cell: column.cell,
footer: column.footer,
};
}
return { accessor };
};
That will make it easy to map over, but the any
problem is still there in the output:
columns[0].cell?.(1) // no error
That should conceivably error with "number
is not assignable to {ok?: number, no?: string}
". But it doesn't. Oh well, you didn't ask about that, and fixing it would take a bunch more refactoring.