How type tabular data with metadata in TypeScript?

Viewed 86

We have the following format from out backend, which has data part that is just tabular data and a meta part that describes the columns in the table. The metadata holds the information about the type for each column.

Example

{
 meta: [
  {name: "foo", type: "NUMBER"},
  {name: "bar", type: "STRING"},
  {name: "baz", type: "TIMESTAMP"},
 ],
  data:[
    [1, "a", 12121232],
    [2, "b", 12121232],
    [3, "c", 12121232],
  ]
}

Is there any way to type the relation between meta and data in TypeScript?

The goal is to have this function to be typechecked successfully, so i can use the type info from the meta info instead of checking the content of every table cell:

const fn = (data:Data) => {
  if(data.meta[1].type ==='STRING'){
    data.data[0][1].concat('-bar')
  }
}
2 Answers

I actually typed up something similar recently. Inspired by that, here's what I came up with for your situation (playground):

type FieldType = 'NUMBER' | 'STRING' | 'TIMESTAMP';

// Suppress warning generated by unused type parameter `S`
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface TypedField<T extends FieldType, S> {
  name: string;
  type: T;
}

type NumberField = TypedField<'NUMBER', number>;
type StringField = TypedField<'STRING', string>;
type TimestampField = TypedField<'TIMESTAMP', number>;

type Field = NumberField | StringField | TimestampField;

type TypeofField<F> = F extends TypedField<infer T, infer S> ? TypedField<T, S> extends Field ? S : never : never;

interface Data {
  meta: Array<Field>;
  data: Array<{ [K in Extract<keyof this['meta'], number>]: TypeofField<this['meta'][K]>; }>;
}

const data: Data = {
  meta: [
    { name: "foo", type: "NUMBER" },
    { name: "bar", type: "STRING" },
    { name: "baz", type: "TIMESTAMP" },
  ],
  data: [
    [ 1, "a", 12121232 ],
    [ 2, "b", 12121232 ],
    [ 3, "c", 12121232 ],
  ],
};

The solution is to use the type predicates. As we know that data[0][0] is a string when meta[0].type is a string, we can tell Typescript it should just assume that it is a string. So adapted @MTCoters answer using as to make it work:

type FieldType = 'NUMBER' | 'STRING' | 'TIMESTAMP';

// Suppress warning generated by unused type parameter `S`
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface TypedField<T extends FieldType, S> {
  name: string;
  type: T;
}

type NumberField = TypedField<'NUMBER', number>;
type StringField = TypedField<'STRING', string>;
type TimestampField = TypedField<'TIMESTAMP', number>;

type Field = NumberField | StringField | TimestampField;

type TypeofField<F> = F extends TypedField<infer T, infer S> ? TypedField<T, S> extends Field ? S : never : never;

interface Data {
  meta: Array<Field>;
  data: Array<{ [K in Extract<keyof this['meta'], number>]: TypeofField<this['meta'][K]>; }>;
}

const data: Data = {
  meta: [
    { name: "foo", type: "NUMBER" },
    { name: "bar", type: "STRING" },
    { name: "baz", type: "TIMESTAMP" },
  ],
  data:[
    [ 1, "a", 12121232 ],
    [ 2, "b", 12121232 ],
    [ 3, "c", 12121232 ],
  ],
};

const fn = (data:Data) => {
  if(data.meta[1].type ==='STRING'){
    (data.data[0][1] as String).concat('-bar')
  }
}

Note that it would fail when removing as String.

Related