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;
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());