slides used in the presentation
building the generic component
In this example we'll use a tale to demostrate how to build a generic component that adapts to any type of data
to go from a fully
hardcoded componenst to a
more dynamic one
we first put
the tables rows into an array and then pass that array to the component which we
loop over to get the table rows
And the same can also be done to
the columns
and now our table mponent can take in the data and the columns and then render
the table rows and columns
type Player = {
id: number;
name: string;
age: number;
rank: number;
}
type TableColumn = {
label: string;
accessor: keyof Player;
};
interface DynamicPlayerstableProps {
columns: Array<TableColumn>;
data: Array<Player>;
}
export function DynamicPlayerstable({data,columns}: DynamicPlayerstableProps) {
return ....
}
is now decoupled from the data but we can take this further by decouping it from
the shape of data. In it's current form it requires us to pass in data like this
const columns: Array<TableColumn> = [
{ label: "ID", accessor: "id" },
{ accessor: "name", label: "Name" },
{ accessor: "age", label: "Age" },
{ accessor: "rank", label: "Rank" },
];
const data: Array<Player> = [
{ id: 1, name: "Player1", age: 25, rank: "Silver" },
{ id: 2, name: "Player2", age: 30, rank: "Gold" },
{ id: 3, name: "Player3", age: 22, rank: "Bronze" },
{ id: 4, name: "Player4", age: 28, rank: "Platinum" },
{ id: 5, name: "Player5", age: 24, rank: "Diamond" },
{ id: 6, name: "Player6", age: 27, rank: "Silver" },
{ id: 7, name: "Player7", age: 29, rank: "Gold" },
];
return <DynamicPlayerstable columns={columns} data={data} />;
The table structure is dynamic enough to accomodate any array of objects we pass
in with the only required field being od id
because we use it as the key to
every row
<tbody>
{data.map((player) => (
<tr key={player.id}>
{columns.map((column) => (
<td key={column.accessor}>{player[column.accessor]}</td>
))}
</tr>
))}
</tbody>
To accomplish this we'll need to use a generic type usuaally maerked as T
which is a way to pass in variables to out types and interfaces
firdt we pass it into the component
export function DynamicPlayerstable<T>({ data, columns }: DynamicPlayerstableProps);
then we pass it into our parameter interface
interface DynamicPlayerstableProps<T> {
columns: Array<TableColumn>;
data: Array<Player>;
}
export function DynamicPlayerstable<T>({ data, columns }: DynamicPlayerstableProps<T>);
this type T
should be the type of the object that we pass in to the array so we can now replace type Player
with T
interface DynamicPlayerstableProps<T> {
columns: Array<TableColumn>;
data: Array<T>;
}
our TbaleColumn
type was also relying on the type Player
so the type is also going to change to TableColumn<T>
so we can now replace type Player
with T
At this point typescipt will be able to give us auto complete for the coluns field based on what type T
is based on the array we pass into the data field
Out component is working fine from the outside but typescript is having a hader time understanding the types inside the component sine generice type T
could be any type and it's keys could be numver|string|symbol
, symbols
aren't allowed as react keys so we can narrow that type by using an intersection &
to inform typescrpt that the keys of T must be a string like we had in type Player
type TableColumn<T> = {
label: string;
accessor: keyof T & string;
};
interface DynamicPlayerstableProps<T> {
columns: Array<TableColumn<T>>;
data: Array<T>;
}
That resolves that issue but introduces another one where typescipt doesn't know what type T
is and what type T[keyof T]
is going to resolve to
Because in it's current shape bothe
type Player = {
id: number;
name: string;
age: number;
rank: string;
};
// and
type playeyWithArrays = {
id: Array<number>;
name: Array<string>;
age: Array<number>;
rank: Date;
ratio: {
numerator: number;
denominator: number;
};
};
[!TIP]
any type can be passed into the component as typeT
, react and our table doesn't expect that and will throw an error if we try to render an array orDate
object in atd
or any react node . so to further specify what inputs we expect we can use theextends
operator
[!NOTE]
in typescript it's either used to inherit behaviour from a class or to mark a generic type as a subtype of another more specific type
T extends string // can only accept strings
T extends {} // can only accept objects
T extends {id:number} // can only accept objects with a key `id` that is a number
T extends Record<string, string | number> // can only accept objects with string or number keys
We'll use this to specify that only objects with string or number keys can be passed into the table
[!NOTE]Record<TKey, TValue>
can be used to specify objects
type TableColumn<T extends Record<string, string|number >> = {
label: string;
accessor: keyof T & string;
};
interface GenericTableProps<T extends Record<string, string|number >> {
columns: Array<TableColumn<T>>;
data: Array<T>;
}
export function GenericTable<T extends Record<string, string|number >>
now with this we can only pass in objects that have string or number keys
const data = [
{
id: 1,
name: "John Doe",
age: 30,
rank: "Gold",
},
];
const baddata = [
{
id: 1,
name: "John Doe",
age: [30],
rank: new Date(),
},
];
before restrictions
after restrictions
one last thing is rqeire type T
to include a key id
because it's going to be used as the key for the table rows
type GenericItem = Record<string, string | number> & { id: string }; // field of id:string required in passed in objects
type TableColumn<T extends GenericItem> = {
label: string;
accessor: keyof T & string;
};
interface GenericTableProps<T extends GenericItem> {
columns: Array<TableColumn<T>>;
data: Array<T>;
}
export function GenericPlayerstable<T extends GenericItem>({ data, columns }: GenericTableProps<T>);
[!TIP]
Bonus tip : handling nested objects
const data = [
{
id: "1",
name: "John Doe",
age: 30,
rank: "Gold",
ratio: {
numerator: 1,
denominator: 2,
},
},
];
This type will cause issues because field ratio is an object and we can't render it directly in the table because react can't render objects as its children
First the Record type should also intersect with an object
type
// now lets the value of the record to be an object
type GenericItem = Record<string, string | number | object> & { id: string };
type TableColumn<T extends GenericItem> = {
label: string;
accessor: PossibleNestedUnions<T, 10> & string;
};
interface GenericTableProps<T extends GenericItem> {
title:string;
description?:string;
columns: Array<TableColumn<T>>;
data: Array<T>;
}
Then we add checks to handle object types
<tbody>
{data.map((row) => (
<tr key={row.id}>
{columns.map((column) => {
const value = row[column.accessor];
// this line grabs the nested object and maps it to a <td>
if (column.accessor.includes(".")) {
return <td key={column.accessor}>{getNestedProperty(row, column.accessor)}</td>;
}
// we've already checked if the accessor is a nested object but typescript is not yet aware and
// still thinks value is type string | number | objct at this point
// this part is only here to narrow the type or catch objects types that fell through
// so that the value type on the section below is of type string | number
if (typeof value === "object") {
const nestedValue = getNestedProperty(row, column.accessor)
if(typeof nestedValue !== "string" || typeof nestedValue !== "number") {
// to avoid accidentally trying to renedr objects as react children
return <td key={column.accessor}>{JSON.stringify(nestedValue)}</td>;
}
return <td key={column.accessor}>{getNestedProperty(row, column.accessor)}</td>;
}
// value type is string| number , safe to render ina react td
return <td key={column.accessor}>{value}</td>;
})}
</tr>
))}
</tbody>
getNestedProperty
is a utility function to retrieve a nested property value from an object based on a dot-separated path
example
const obj = { a: { b: { c: 42 } } };
console.log(getNestedProperty(obj, "a")); // Outputs: { b: { c: 42 } }
console.log(getNestedProperty(obj, "a.b.c")); // Outputs: 42
console.log(getNestedProperty(obj, "a.b.x")); // Outputs: undefined
so how do we get the types to the dot separated values? this one is non trivial and one of the examples of the ugly typescript people hate.PossibleNestedUnions
uses recursion to extract the dot separated keys to inputs of type nested object
example
type ExampleType = {
a: string;
b: {
c: number;
d: {
e: boolean;
f: {
g: string;
};
};
};
h: Date;
};
type NestedKeys1 = PossibleNestedUnions<ExampleType, 1>; // "a" | "b" | "h"
type NestedKeys2 = PossibleNestedUnions<ExampleType, 2>; // "a" | "b" | "b.c" | "b.d" | "h"
type NestedKeys3 = PossibleNestedUnions<ExampleType, 3>; // "a" | "b" | "b.c" | "b.d" | "b.d.e" | "b.d.f" | "h"
type NestedKeysAll = PossibleNestedUnions<ExampleType>; // includes all nested paths
and now our new type will be
import { PossibleNestedUnions } from "../types/nested_objects_union";
import { getNestedProperty } from "../utils/object";
type GenericItem = Record<string, string | number | object>&{id:string}
type TableColumn<T extends GenericItem> = {
label: string;
accessor: PossibleNestedUnions<T> & string;
};
interface GenericTableProps<T extends GenericItem> {
columns: Array<TableColumn<T>>;
data: Array<T>;
}
with the array
const gooddata = [
{
id: "1",
name: "John Doe",
age: 30,
rank: "Gold",
stats:{
running:20,
jumping:10,
swimming:30,
weapons:{
stars:10,
katana:5
}
}
},
{
id: "2",
name: "Pedro",
age: 30,
rank: "Gold",
stats:{
running:20,
jumping:10,
swimming:30,
weapons:{
stars:1,
katana:4,
blades:{
short:10,
long:5
}
}
}
},
];
we'll get auto complete for all the possibly nested types
Finally we can make the title and description of the table dynamic too because we can render more than just players with this component
<GenericTableWithNestedFields
title="Players Table"
description="list of players without their stats"
data={gooddata}
columns={[
{ accessor: "id", label: "age" },
{ accessor: "name", label: "name" },
{ accessor: "age", label: "age" },
{ accessor: "rank", label: "rank" },
]}
/>
<GenericTableWithNestedFields
title="Players Table with stats"
description="list of players with their stats"
data={gooddata}
columns={[
{ accessor: "id", label: "age" },
{ accessor: "name", label: "name" },
{ accessor: "age", label: "age" },
{ accessor: "rank", label: "rank" },
{ accessor: "s
Author Of article : Dennis kinuthia
Read full article