reactjstypescript

Assign a property to be a key of a generic type in TypeScript


I am trying to create a reusable Table component in React, the idea being that you can pass in a list of columns, and the data to generate a table. Let's say the data was an array of people - Person[], I want to call something like this:

type Person {
  name: string;
}

const people = [
  { name: "John Smith" }
];

const columns = [
  {label: "Name", path: "name"}
];

return <Table<Person> data={people} columns={columns} />

So in the type definition of the columns array, the path property should be a keyof the data type (i.e. Person). Since Table is a reusable component, I am trying to achieve this with generic types and a keyof T, however I am clearly missing something here as the TS compiler is telling me: Type 'string' is not assignable to type 'keyof Person'.

Here's the code for my Table component:

import TableBody from "./TableBody";
import TableHeader from "./TableHeader";

export interface Column<T> {
    label: string;
    path: keyof T;
    key?: number | string;
    finder?: () => string;
    content?: (arg: T) => string | React.JSX.Element;
}

interface Props<T> {
    data: T[];
    columns: Column<T>[];
}

const Table = <T extends { id: number }>({ data, columns }: Props<T>) => {
    return (
        <div className="relative overflow-x-auto">
            <table className="w-full text-left rtl:text-right text-gray-500">
                <TableHeader<T> columns={columns} />
                <TableBody<T> data={data} columns={columns} />
            </table>
        </div>
    );
};

export default Table;

Solution

  • The type that is inferred for columns is

    {
        label: string;
        path: string;
    }[]
    

    because it would be annoying for array literals to infer strict types by default.

    If you want it to infer a stricter type from the literals, you need to mark (parts of it) as const. If you mark the whole columns as const, then you will need to adjust the definition of Props to be readonly.

    N.b. you are also missing id: number on your definition of Person.

    interface Props<T> {
        data: T[];
        columns: readonly Column<T>[];
    }
    
    const Table = <T extends { id: number }>({ data, columns }: Props<T>) => {
        return (
            <div className="relative overflow-x-auto">
                <table className="w-full text-left rtl:text-right text-gray-500">
                    <TableHeader<T> columns={columns} />
                    <TableBody<T> data={data} columns={columns} />
                </table>
            </div>
        );
    };
    
    type Person = {
      id: number;
      name: string;
    }
    
    const people = [
      { id: 1, name: "John Smith" }
    ];
    
    const columns = [
      {label: "Name", path: "name"}
    ] as const;
    
    const People = () => {
      return <Table<Person> data={people} columns={columns} />
    }
    

    Playground link