Some time ago, I came across the task of creating a table component for one of the applications I work on. Although it's a simple task, there are many opportunities when we talk about Developer Experience.
Let's start with something very simple, a component that takes two properties: data
and columns
.
function TableHead(props: Pick<Props, 'columns'>) {
return <>...</>
}
function TableBody(props: Props) {
return <>...</>
}
export function Table({ data, columns }: Props) {
return (
<table>
<TableHead columns={columns} />
<TableBody columns={columns} data={data} />
</table>
)
}
I'll set aside the implementation of the component logic itself to make it easier to focus on what really matters for this post.
The data
property here refers to the data that our table needs to render. To simplify things, we will consider a simple table that accepts string
, number
, or boolean
as possible values, for example.
[{
name: 'Pedro Henrique Pires',
age: 27,
github_url: 'http://github.com/pedrohenriquepires'
}]
As for the columns
property, it refers to the mapping of the columns in our table. We might want to have GitHub URL
written in the header of our table instead of github_url
.
For this purpose, we can define that our column object will have two properties: title
and accessor
, where title
is the property that sets the column name, and accessor
is the property that defines which property of the data object this column will render.
For typing this component, we can use something like this:
type TableData = Record<string, string | number | boolean>
type TableColumn = {
title: string
accessor: string
}
type Props = {
data: TableData[]
columns: TableColumn[]
}
With this, we already have a properly typed component... do we?
Right away, it's possible to notice a problem that could potentially cause bugs in our component.
The accessor
property of our column doesn't have any awareness of the properties that exist in our data, meaning it's very easy for someone to make a typing mistake and write gtihub_url
instead of github_url
, causing a failure in rendering that column.
Not to mention that we don't have any autocomplete assistance when configuring the columns, making it a bit more challenging for those using the component.
Fortunately, there is a solution to these problems, and it's much simpler than it may seem.
Generic Types
With a few small modifications to our code, we can turn our component into a generic component and leverage TypeScript's inference to our advantage.
type TableData<T> = {
[Property in keyof T]: string | number | boolean
};
type TableColumn<T> = {
title: string
accessor: keyof T
}
type Props<T> = {
data: TableData<T>[]
columns: TableColumn<T>[]
}
We changed the type TableData
to receive a generic type and used this type for typing the properties of the object.
We also changed the type TableColumn
to receive a generic type, using the properties of this generic type (keyof) as possible values for our accessor
property.
The type Props
, which we use to define the properties of our component, was also changed to receive a generic type, and this type is passed to the TableData
and TableColumn
types.
To finish, we also need to modify our component:
function TableHead<T>(props: Pick<Props<T>, 'columns'>) {
return <></>
}
function TableBody<T>(props: Props<T>) {
return <></>
}
export function Table<T>({ data, columns }: Props<T>) {
return (
<table>
<TableHead columns={columns} />
<TableBody columns={columns} data={data} />
</table>
)
}
The change here is also simple; we just added a generic parameter to the component and passed that parameter to the Props
type.
Now, the accessor
property has awareness of the properties available within data
. We will start to have autocomplete to assist in using the component and errors when using properties that do not exist, preventing bugs in our application.
Conclusion
With a few changes, we've managed to enhance the development experience and make our component more secure. If you're not yet familiar with generics, I strongly recommend checking out some materials on the topic. This video by Matt Pocock is an excellent guide to get started.
Disclaimer
With great power comes great responsibility. Often, we can end up adding unnecessary complexity to our code. This article demonstrates just one use case and should not be taken as a rule.
Top comments (0)