4

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?

6
  • 1
    Like this, maybe? TypeScript doesn't let you do this with a class declaration, since for class and interfaces, the keys must be statically known. Instead you need to write use a type assertion or other declaration to tell the compiler that MyClass acts as a constructor producing some object type with dynamic keys. If that works for you I could maybe write up an answer (although I'm 85% sure I've written up that answer before so I should find it maybe). If it doesn't work please edit the code in the question to demonstrate unsatisfied use cases. Commented Nov 21, 2021 at 4:50
  • This works! I edited your example for clarity. Commented Nov 21, 2021 at 12:07
  • Although i have some difficulty understanding this line: "as new <T extends Record<string, unknown>>(arg: T) => MyClassType<T>" Commented Nov 21, 2021 at 12:27
  • what does the "new" operator mean as a type? Commented Nov 21, 2021 at 12:52
  • I’m not sure that the change from object to Record<string, unknown> is an improvement in clarity. I imagine you made that change to appease some (misguided in my opinion) linter rule but that’s not really making anything more clear (especially if you try to use an interface type for T and find out that it matches object but not Record<string, unknown>). Unless your question is about such things, I plan to leave object in the answer I post (when I get a chance to write it up). See github.com/microsoft/TypeScript/issues/… Commented Nov 21, 2021 at 12:54

2 Answers 2

4

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

Sign up to request clarification or add additional context in comments.

Comments

0

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

1 Comment

Thank you, but i would like to call the class with the "new" operator. So this does not work for me. But again thank you!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.