Suppose I have the following definitions:
export enum Category {
car = 'car',
truck = 'truck',
}
export interface IProperty {
propertyName: string,
propertyLabel: string,
propertyType: 'string' | 'number',
}
export interface CategoryDefinition {
category: Category,
label: string,
properties: IProperty[]
}
and then have definitions like that:
export class Definitions {
public static get car(): CategoryDefinition {
return {
category: Category.car,
label: "Car",
properties: [
{
propertyName: 'weight',
propertyLabel: 'Weight',
propertyType: 'number'
},
{
propertyName: 'color',
propertyLabel: 'Color',
propertyType: 'string'
}
],
};
}
public static get truck(): CategoryDefinition {
return {
category: Category.truck,
label: "Truck",
properties: [
{
propertyName: 'payload',
propertyLabel: 'Payload',
propertyType: 'number'
},
{
propertyName: 'axles',
propertyLabel: 'Axles',
propertyType: 'number'
}
],
};
}
}
And now, I would like to generate interfaces from this definition, which should result in this:
export interface Car {
weight: number,
color: string
}
export interface Truck{
payload: number,
axles: number
}
Ideally done someway like:
export interface Car = createInterface('Car', Definitions.car.properties);
export interface Truck = createInterface('Truck', Definitions.truck.properties);
Reason being that I have to deal with about 30-40 of those definitions. I can of course define their interface on their own (API responses), but then I would have to re-define their properties for Ui display, and this would be error-prone. This way I would only have to define their properties and could generate their corresponding interface.
I have found an answer that maybe hint in the right direction, however I feel that it's not quite the same and I could not get it to work.
The Definitions would be fixed (i.e. not dynamically changed/updated), if that helps.
Any help would be appreciated.
CodePudding user response:
You should probably make the definition types readonly (as you shouldn't need to change these at runtime):
export interface IProperty {
readonly propertyName: string,
readonly propertyLabel: string,
readonly propertyType: 'string' | 'number',
}
export interface CategoryDefinition {
readonly category: Category,
readonly label: string,
readonly properties: readonly IProperty[]
}
And another change needed is to use as const satisfies CategoryDefinition
instead of annotating the return type to be CategoryDefinition
. This "preserves" the values while still making sure it is of type CategoryDefinition
.
export class Definitions {
public static get car() {
return {
category: Category.car,
label: "Car",
properties: [
{
propertyName: 'weight',
propertyLabel: 'Weight',
propertyType: 'number'
},
{
propertyName: 'color',
propertyLabel: 'Color',
propertyType: 'string'
}
],
} as const satisfies CategoryDefinition;
}
public static get truck() {
return {
category: Category.truck,
label: "Truck",
properties: [
{
propertyName: 'payload',
propertyLabel: 'Payload',
propertyType: 'number'
},
{
propertyName: 'axles',
propertyLabel: 'Axles',
propertyType: 'number'
}
],
} as const satisfies CategoryDefinition;
}
}
Then we need to define a type that maps the property types to their actual types:
type TypeMap = {
string: string;
number: number;
// etc ...
};
Finally, we can map over the definition properties and retrieve the right type for each field:
type FromDefinition<D extends CategoryDefinition> = {
[T in D["properties"][number] as T["propertyName"]]: TypeMap[T["propertyType"]];
};
type Car = FromDefinition<typeof Definitions["car"]>;
// ^? { weight: number; color: string }
type Truck = FromDefinition<typeof Definitions["truck"]>;
// ^? { payload: number; axles: number }
Does your Definitions class really need to be a class? It could just be:
const Definitions = {
car: { ... },
truck: { ... },
} as const satisfies Record<string, CategoryDefinition>;
and that'd be simpler to read and maintain.
CodePudding user response:
You'd have to create custom types for this, which would take a bit of work (usually it's recommended to use a third-party library like Zod to define a "schema" for your types that you also want to access in TypeScript. But, I'll see if I can come up with some types that work for your use case if you don't want to use a third-party library.
There are a few things you need to tweak with your definitions (which vera explains in the other answer), but here's the "magic" type I came up with:
type TypeMap = {
string: string,
number: number,
}
export type GetDefinitionType<D extends CategoryDefinition> = {
[Property in D['properties'][number] as Property['propertyName']]:
TypeMap[Property['propertyType']]
}
What it does is loops over all the properties and then "maps" the stringified type to the actual TypeScript type using the TypeMap
type ("string" -> string
, "number" to number
) (credit to vera for this idea)
The problem is, as your types get more complex, you'd need to maintain a mapping of these values yourself, which is why third-party libraries like zod exist; to handle these value -> type mappings for you (which I recommend using).