Home > Enterprise >  Use typescript to dynamically set class property in constructor
Use typescript to dynamically set class property in constructor

Time:11-22

I am trying create a typescript class MyClass with instance properties set dynamically in the constructor:

const myInstance = new MyClass(({
  someField: 'foo'
}))

myInstance.someField // typescript should show this as type string

How can I use typescript to create MyClass to show someField as a string property of myInstance?

Can I use generics? Is it at all possible?

CodePudding user response:

Conceptually you want to say something like class MyClass<T extends object> extends T or class MyClass<T extends object> implements T, but TypeScript will not allow a class instance or an interface to have dynamic keys. All keys of a class or interface declaration must be statically known. So if you want this behavior you will need to do something other than a class declaration.

Instead, you can describe the type of your desired MyClass<T> instances as well as the type of the MyClass class constructor, and then use a type assertion to tell the compiler that your actual constructor object is of that type.

Let's imagine that your intended MyClass<T> class instances have all the properties of T, plus a method named someMethodIGuess(). Then your MyClass<T> type can be defined like this:

type MyClass<T extends object> = T & {
    someMethodIGuess(): void;
}

You want your MyClass class constructor to have a construct signature that takes an argument of type T for some generic T object type, and produces an instance of MyClass<T>. That can be defined like this:

type MyClassConstructor = {
    new <T extends object>(arg: T): MyClass<T>
}

To implement this class we can use [Object.assign()] to copy the constructor argument properties into the instance, plus any methods or other things we need.

const MyClass = class MyClass {
    constructor(arg: any) {
        Object.assign(this, arg);
    }
    someMethodIGuess() {

    }
} 

But of course if we leave it like this, the compiler will not see MyClass as a MyClassConstructor (after all, it can't have dynamic keys, and I didn't even try to tell it about T here). We need a type assertion to tell it so, like this:

const MyClass = class MyClass {
    constructor(arg: any) {
        Object.assign(this, arg);
    }
    someMethodIGuess() {

    }
} as MyClassConstructor;

That compiles with no error. Again, note that the compiler is unable to understand that the MyClass implementation conforms to the MyClassConstructor type. By using a type assertion, I've shifted the burden of ensuring that it is implemented correctly away from the compiler (which can't do it) to me (or you if you use this code). So we should be very careful to check what we've done and that we didn't write the wrong thing (e.g., Object.assign(arg, this); instead of Object.assign(this, arg); would be a problem).


Let's test it out:

const myInstance = new MyClass(({
    someField: 'foo'
}))

console.log(myInstance.someField.toUpperCase()); // FOO
myInstance.someMethodIGuess(); // okay

Looks good! The compiler expects myInstance to have a someField property of type string, as well as that someMethodIGuess() method.


So that's it. Note that there are caveats around using MyClass in the same way you'd use another class constructor. For example, the compiler will never be happy with a class declaration that has dynamic keys, so if you try to make a generic subclass of MyClass with a class declaration, you'll get an error:

class SubClass<T extends object> extends MyClass<T> { // error    
    anotherMethod() { }
}

If you need that sort of thing you'll find yourself having to use the same trick as before with the subclass:

type SubClass<T extends object> = MyClass<T> & { anotherMethod(): void };
type SubClassConstructor = { new <T extends object>(arg: T): SubClass<T> };
const SubClass = class SubClass extends MyClass<any> {
    anotherMethod() { }
} as SubClassConstructor;

Playground link to code

CodePudding user response:

How about this?

class MyClass{
    constructor(props: any) {
        Object.assign(this, props);
    }

    baz(): string {
        return "Hello World";
    }

    static create<T>(props = {} as T) {
        return new MyClass(props || {}) as MyClass & T
    }
}

const o = MyClass.create({
    foo: 1,
    bar: "Hello World"
});

console.log(o.foo);
console.log(o.bar);
console.log(o.baz());


TS playground

  • Related