Home > database >  Infer a record type from an Array type generic parameter
Infer a record type from an Array type generic parameter

Time:10-18

I'm trying to write a type for a generic table component that accepts a list of columns (each column has a name and type) and a list of rows that need to conform to the shape dictated by the columns.

My code only works when I add explicit types, but I'd much prefer if typescript could infer those for me:

enum ColumnType {
  text = "text",
  numeric = "numeric",
}

interface Props<T extends Record<string, ColumnType>> {
  columns: Array<{ name: keyof T; columnType: ColumnType }>;
  rows: Array<
    {
      [K in keyof T]: T[K] extends ColumnType.numeric
        ? { content: number }
        : { content: string };
    }
  >;
}

function consume<T extends Record<string, ColumnType>>(arg: Props<T>): void {}

// missing fields, should not typecheck
consume({
  columns: [
    { name: "a", columnType: ColumnType.numeric },
    { name: "b", columnType: ColumnType.text },
  ],
  rows: [{}], 
});

// type mismatch, should not typecheck
consume({
  columns: [
    { name: "a", columnType: ColumnType.numeric },
    { name: "b", columnType: ColumnType.text },
  ],
  rows: [{ a: { content: "asdf" }, b: { content: 42 } }], 
});

// should typecheck, but doesn't
consume({
  columns: [
    { name: "a", columnType: ColumnType.numeric },
    { name: "b", columnType: ColumnType.text },
  ],
  rows: [{ a: { content: 42 }, b: { content: "asdf" } }],
});

// works with explicit type argument
consume<{ a: ColumnType.numeric; b: ColumnType.text }>({
  columns: [
    { name: "a", columnType: ColumnType.numeric },
    { name: "b", columnType: ColumnType.text },
  ],
  rows: [{ a: { content: 42 }, b: { content: "asdf" } }],
});

Is there a way to accomplish this with typescript?

Typescript playground

CodePudding user response:

The first step would be to capture more information about the columns. We can do this by making the whole array a type parameter. To make sure we capture literal types, we will add some type parameters for the names and column types of props to help TS infer literal types for those fields instead on inferring their wider types.

function consume<T extends Array<{ name: K; columnType: CT }>, K extends PropertyKey, CT extends ColumnType>(arg: Props<T>): void {}

We would also like to capture a tuple type instead of an array type to more easily process each element of the array. To do this, we can add a [] | to the type constraint:

function consume<T extends [] | Array<{ name: K; columnType: CT }>, K extends PropertyKey, CT extends ColumnType>(arg: Props<T>): void {}

In order to map the elements of the array to fields in an object, will need to use a mapped type.

Here we have two options.

The first option would be to use the as clause in mapped types to transform each element of the tuple to a field. We will need to filter only the indices of the tuple (we don't need the Array members). To do this we can intersect with `${number}` (tuple indices are actually represented as strings in the type system "0", "1" ... )

type Row<T extends Array<{ name: PropertyKey; columnType: ColumnType }>> =  {} & {
  [K in keyof T & `${number}` as T[K]['name']]: T[K]['columnType'] extends ColumnType.numeric
      ? { content: number }
      : { content: string };
}

The other option, compatible with older versions of TS would be to index with number to get a union of all elements of the tuple, map over the union resulting from the name property, then to get the type of each field, extract the corresponding union constituent using `Exatct:

type Row<T extends Record<number, { name: PropertyKey; columnType: ColumnType }>> =  {} & {
    [K in T[number]['name']]: Extract<T[number], { name: K } >['columnType'] extends ColumnType.numeric
      ? { content: number }
      : { content: string };
  }

Putting it all together we get:

enum ColumnType {
  text = "text",
  numeric = "numeric",
}

type Row<T extends Array<{ name: PropertyKey; columnType: ColumnType }>> =  {} & {
  [K in keyof T & `${number}` as T[K]['name']]: T[K]['columnType'] extends ColumnType.numeric
      ? { content: number }
      : { content: string };
}
type Props<T extends Array<{ name: PropertyKey; columnType: ColumnType }>> = {
  columns: T;
  rows: Array<Row<T>>;
}

function consume<T extends [] | Array<{ name: K; columnType: CT }>, K extends PropertyKey, CT extends ColumnType>(arg: Props<T>): void {}

Playground Link

  • Related