Svish
Svish

Reputation: 158321

How to link keyof types within an interface

Not sure if this is possible, but it's related to keyof which I'm having a hard time wrapping my mind around...

I currently have this interface:

interface Column<T> {
  header: string;
  field: keyof T;
  format?: (value: any) => string;
}

First usage for it is to create something that renders a table, given a list of objects and columns.

const rows = [{name: 'Alice', age: 18}, {name: 'Bob', age: 12}];

type Row = typeof rows[0];

const columns: Column<Row>[] = [
  {
    header: 'Name',
    field: 'name',
    format: value => value.toUpperCase(),
  },
  {
    header: 'Age',
    field: 'age',
    format: value => value.toLocaleString(),
  },
];

const renderTable = <T>(rows: T[], columns: Columns<T>[]) { ... }
renderTable(rows, columns);

Is there a way to get rid of the any type here in the format function? To somehow "link" the keyof T of field with the value of format? I.e. for the first column value should be string, and for the second it should be number?


(I know it would be very easy to just print these out manually, but there's also going to be added some auto-magic stuff to this table which will make use of field and format)

Upvotes: 0

Views: 629

Answers (1)

skovy
skovy

Reputation: 5650

One approach is to rely on TypeScript's mapped types.

This will "map" through every key (Key in keyof) in the generic passed (Row).

Then for each Row key (eg: name, age, admin) we'll assign that an object of the shape you outlined with the properties header, field and format.

However, since we now know we're working with a specific key (eg: age) we can use Key to reference age in this mapped typed and Row[Key] to reference the type of age (number).

Finally, you don't want keys with nested objects, you want only the objects so we can effectively "pluck" those values with [keyof Row], looking up all the values for "age" | "name" | "admin" and what is left is the union type of all the possible columns with the field and format correctly typed.

type Columns<Row> = {
  [Key in keyof Row]: {
    header: string;
    field: Key,
    format?: (value: Row[Key]) => string;
  }
}[keyof Row]

const rows = [{name: 'Alice', age: 18, admin: true }, {name: 'Bob', age: 12, admin: false}];

type Row = typeof rows[0];

const columns: Columns<Row>[] = [
  {
    header: 'Name',
    field: 'name',
    format: value => value.toUpperCase(), // value: string
  },
  {
    header: 'Age',
    field: 'age',
    format: value => value.toLocaleString(), // value: number
  },
 {
    header: 'Admin',
    field: 'admin',
    format: value => value.toString(), // value: boolean
  },
];

TypeScript Playground

Upvotes: 2

Related Questions