Consider the following type for a column within a data table:
interface Column<T> {
key: keyof T;
title: string;
format?: (value: T[keyof T], record: T) => string | number;
}
T
is the data type of the table records, and the value to be displayed in the column is indexed by the key
property. A format
function would perform custom formatting or other processing for a specific column to be shown in the table, and so accepts the value of the column, which is T[keyof T]
, and the record itself.
The issue this presents is that T[keyof T]
represents a union type of all data types contained within in type T
.
If, for example, T
were a Person with the type:
interface Person {
name: string;
weight: number;
birthday: Date;
}
then T[keyof T]
would equal string | number | Date
. But, when an object is instantiated with a format
function, the specific type of the value
cannot be determined.
const column: Column<Person> = {
key: 'birthday',
title: "Birth Date",
render: (value) => value.getFullYear();
}
This would throw an error because TypeScript cannot be certain that value
is in fact of type Date
and so contains the getFullYear()
method.
I know that it’s possible to add a second generic argument to the type, like so:
export interface Column<T, K extends keyof T> {
key: K;
title: string;
format?: (value: T[K], record: T) => string | number;
}
In this case, it is possible to type the previous example correctly by specifying the key name:
const column: Column<Person, 'birthday'> = {
key: 'birthday',
title: "Birth Date",
format: (value) => value.getFullYear()
}
But this is not practically useful, as almost all usages of a Column
would exist in an Array with a single type declaration.
Is it somehow possible to access the specific instanced value of the key
property in an interface/type, or create an auxiliary type that does so, such that you can define a property which is the type of the specific property indexed by that exact key?
I know I can simply resolve these errors with type assertions, but it bothers me that I can’t find a cleaner way to do it.
>Solution :
This approach uses a ‘throwaway’ Mapped type to create a union of all valid Column structures, but where each one is key-specific. You can hover over value
in the Typescript Playground to prove it…
interface Person {
name: string;
weight: number;
birthday: Date;
}
type Column<T> = {
[K in keyof T]: {
key: K;
title: string;
format?: (value: T[K], record: T) => string | number;
};
}[keyof T];
const column: Column<Person> = {
key: "birthday",
title: "The day you were born",
format: (value, record) => "some formatted string",
};