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]);
});
});
};
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]);
});
});
};
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]);
});
});
};
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
.