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 ....
}

Our players table component

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

with player rows array

with teams row array


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 type T , react and our table doesn't expect that and will throw an error if we try to render an array or Date object in a td or any react node . so to further specify what inputs we expect we can use the extends 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