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')
}
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With