Home > Software design >  Use values of a certain key in an array of object as keys
Use values of a certain key in an array of object as keys

Time:06-08

I am trying to define types so that the type of Row depends on the values provided in Column.

For example

type Column = { key: string; title: string };

type Row = {
  // ??
};

const table = (columns: Column[], rows: Row[]) => {};

table(
  [
    { key: "firstName", title: "First Name" },
    { key: "address", title: "Address" },
  ],
  [{ firstName: "mKman", address: "1 First St" }] // how to enforce this ?
);

CodePudding user response:

In order to do that, you need to infer each key property of each column:

type TableColumn = {
  key: string;
  title: string
};


const table = <
  Key extends string,
  Column extends TableColumn & { key: Key },
  Columns extends Column[],
  Row extends Record<Columns[number]['key'], string>,
  Rows extends Row[]
>(columns: [...Columns], rows: [...Rows]) => {};

table(
  [
    { key: "firstName", title: "First Name" },
    { key: "address", title: "Address" },
  ],
  [{ firstName: "mKman", address: "1 First St" }] // ok
);


table(
  [
    { key: "firstName", title: "First Name" },
    { key: "address", title: "Address" },
  ],
  [{ address: "1 First St" }] // expected error, no firstName
);

table(
  [
    { key: "firstName", title: "First Name" },
    { key: "address", title: "Address" },
  ],
  [{ firstName:'John', addres: "1 First St" }] // expected error, addres is written with type, should be double ss
);

Playground

You can find more information about function argument inference in my article

CodePudding user response:

In order capture a type for use somewhere else, you need generics.

const table = <T extends readonly Column[]>(
  columns: T,
  rows: Record<T[number]['key'], string>[]
) => {};

Here the type of the array of columns objects is captured as T.

Then rows uses that type. Indexing an array type by [number] gets the type of the array members. And then indexing that by ['key'] looks up the property named key on those objects.

In the case of your example:

T[number]['key']

Would be of type:

"firstName" | "address"

You can now make a Record type with that union of string literals as keys, that have string as the value type.

Record<T[number]['key'], string>

You also need to make sure you pass in your schema as const in order to capture the string literal types, instead of just string.


Putting all that together:

type Column = { key: string; title: string };

type Row<T extends readonly { key: string }[]> =
  Record<T[number]['key'], string>;

const table = <T extends readonly Column[]>(
  columns: T,
  rows: Row<T>[]
) => {};

table(
  [
    { key: "firstName", title: "First Name" },
    { key: "address", title: "Address" },
  ] as const,
  [
    { firstName: "mKman", address: "1 First St" }, // fine
    { lastName: "mKman", address: "2 Second St" }, // error
  ]
);

Playground

  • Related