Skip to content
Advertisement

Access an object property whose key is defined by another object

I’m writing a function (to generate an HTML table) that takes two parameters: columns and rows.

The idea is that rows is an array of objects which represents the table rows, and columns is an object that defines which properties can be in the rows objects.

I have a type definition for rows which takes a generic argument of columns:

type column = {
  key: string;
  label: string;
};

type rows<T extends column[]> = {
  [key in T[number]["key"]]: string;
};

And I use these types in my table-making function:

interface TableBodyProps<T extends column[]> {
  columns: T;
  tableRows: rows<T>[];
}

const TableBody = <T extends column[]>({
  columns,
  tableRows,
}: TableBodyProps<T>) => {
  tableRows.forEach((row) => {
    columns.forEach((column) => {
      console.log(row[column.key]);
    });
  });
};

But upon trying to access row[column.key] I get an error:

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'rows<T>'

I guess what’s happening is that column.key just returns a string type, so it makes sense that I couldn’t use it to index. So I guess I need to tell typescript that the string I’m using is indeed a key of each row, but I thought that’s what I was doing with the generic parameters in the first place!

Advertisement

Answer

You’ll need to use

type column<K> = {
  key: K;
  label: string;
};

type rows<T extends column<string>[]> = {
  [key in T[number]["key"]]: string;
};

so that column["key"] is not just string but a specific type (K). Then you can do

type TableBodyProps<K extends string, T extends column<K>[]> = {
  columns: column<K>[];
  tableRows: rows<T>[];
}

const TableBody = <K extends string, T extends column<K>[]>({
  columns,
  tableRows,
}: TableBodyProps<K, T>) => {
  tableRows.forEach((row) => {
    columns.forEach((column) => {
      console.log(row[column.key]);
    });
  });
};

(playground)

or just

type TableBodyProps<K extends string> = {
  columns: column<K>[];
  tableRows: rows<column<K>[]>[];
}

const TableBody = <K extends string>({
  columns,
  tableRows,
}: TableBodyProps<K>) => {
  tableRows.forEach((row) => {
    columns.forEach((column) => {
      console.log(row[column.key]);
    });
  });
};

(playground)

where rows<column<K>[]> is just Record<K, string>. The separate T parameter is only useful if you want to do something else with the specific cell type, e.g. pass it to a callback that takes a concrete T.

User contributions licensed under: CC BY-SA
5 People found this is helpful
Advertisement