Home > Software design >  Typescript create interface definition from Object
Typescript create interface definition from Object

Time:01-07

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 }

Playground


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.

Here's a working playground

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).

  • Related