Home > OS >  Access an object property whose key is defined by another object
Access an object property whose key is defined by another object

Time:10-06

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!

CodePudding user response:

You can add a second type parameter to TableBodyProps that is constrained to the first, that way you can say that all column keys must be one of the table row properties

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

type rows<K extends string> = {
  [P in K]: string;
};

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

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

Playground

CodePudding user response:

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.

  • Related