Can TypeScript's @Decorator functionality be applied to dynamically created object

34 Views Asked by At

I need to create an object at runtime and define it's properties. Properties and object itself should be decorated. I need that object to behave the same way as I would define it using class in the source code. Essentially I have dynamic description of a class/object stored in variables.

How can I go from this:

const entityName = 'User'
const entityDecorator = 'Entity'
const entityProperties = [
    { name: "name", type: "string", decorator: { name: 'Column', arguments: [20] } },
    { name: "email", type: "string", decorator: { name: 'Column', arguments: [20, 'email'] } },
]

Object.create({});
// something..

and end up with an object that would behave as it was defined in a normal way in the source code as:

@Entity
export class User {
    @Column(20)
    name: string
    
    @Column(20, 'email')
    email: string
}

I tried this [https://stackoverflow.com/questions/52694469/adding-decorators-to-dynamically-created-functions-in-typescript](Adding decorators to dynamically created functions in typescript) but it didn't help.

1

There are 1 best solutions below

0
Etienne Laurin On

It depends on the library and the decorator, but in many cases code like dynamicClass below might work.

Unlike the sample input, it requires slightly changing the decorators to be actual decorators and not strings.

Like the sample class in the question, this code does not set the value of the properties and would have to be extended to do so.

type Types = { string: string };

type ClassSpec = {
  name: string;
  decorator: ClassDecorator;
  properties: {
    name: string;
    type: keyof Types;
    decorator: MethodDecorator;
  }[];
};

type SpecType<Spec extends ClassSpec> = {
  [K in Spec["properties"][number] as K["name"]]: Types[K["type"]];
};

function dynamicClass<Spec extends ClassSpec>({
  name,
  decorator,
  properties,
}: Spec): new () => SpecType<Spec> {
  let overrides: PropertyDescriptorMap = {};
  const cls = function (this: SpecType<Spec>) {
    Object.defineProperties(this, overrides);
  };
  for (const { name, decorator } of properties) {
    const res = decorator(cls, name, {});
    if (res) {
      overrides[name] = res;
    }
  }
  Object.assign(cls, { name });
  return decorator(cls) as unknown as new () => SpecType<Spec>;
}

It can be used like so:

declare const Entity: ClassDecorator;
declare const Column: (size: number, type?: string) => MethodDecorator;

const entity = {
  name: "User",
  decorator: Entity,
  properties: [
    { name: "name", type: "string", decorator: Column(20) },
    { name: "email", type: "string", decorator: Column(20, "email") },
  ],
} as const satisfies ClassSpec;

const x = new (dynamicClass(entity))();