Reputation: 139
I have a function which creates a class using a generic interface. The properties of the instance are set by a parameters of that generic, like this:
const ClassFactory = <T>() => {
class MyClass {
constructor(data: T) {
for (const key in data) {
if (!data.hasOwnProperty(key)) { continue; }
(this as any)[key] = data[key];
}
}
// other methods can be here
}
return MyClass;
}
const GeneratedClass = ClassFactory<{ id: number }>();
const myInstance = new GeneratedClass({ id: 1 });
console.log((myInstance as any).id); // logs 1
This runs as intended, however there are 2 problems
myInstance
typings doesn't have the keys of T - I'm expecting myInstance.id
to be a numberthis as any
in the constructor to assign the values by the given dataIn an attempt to fix the first problem I've tried various things I've seen from other posts, including
class MyClass implements T
, but they all result in the same error: A class can only implement an object type or intersection of object types with statically known members.ts(2422)
.
I understand why it happens, however since T
is known when defining the Class, is there a way for this to work?
If I have the data in a public data: T
property, then myInstance.data.id
is properly typed. So my question is, can this be done by skipping the .data
part?
Thanks in advance
Upvotes: 4
Views: 3096
Reputation: 5749
I just had this problem, and was able to solve it quite elegantly by simply ditching new
and creating a generic static method Class.create()
instead, where I instantiate and cast as the correct type.
interface MyGenericInterface<T = any> {
getTheThing(): T
}
class MyClass implements MyGenericInterface {
// Make constructor private to enforce contract
private constructor(private thing: any) {}
public getTheThing(): any {
return this.thing
}
static create<TypeOfThing = any>(thing: TypeOfThing) {
return new MyClass(thing) as MyGenericInterface<TypeOfThing>
}
}
const stringyInstanceWrong = new MyClass('thing') // Error
const stringyInstance = MyClass.create('thing')
const stringThing: string = stringyInstance.getTheThing()
const numberyInstance = MyClass.create(123)
const numberyThingWrong: string = numberyInstance.getTheThing() // Error
const numberyThing: number = numberyInstance.getTheThing() // Works!
Upvotes: 0
Reputation: 139
Inspired by Jonas Wilms's comment, I got it to work even if the class has methods/statics by returning
return MyClass as (new (data: T) => T & InstanceType<typeof MyClass>) & typeof MyClass;
Like this, all of the following are typed and run as intended
const myInstance = new GeneratedClass({ id: 1 });
console.log(myInstance.id, GeneratedClass.someStatic(), myInstance.someMethod());
However, this doesn't work properly if new MyClass()
is used inside the class methods.
A workaround for it to be working is to create a static which returns an instance with the proper types
// inside the class
public static build(data: T): T & InstanceType<typeof MyClass> {
return new MyClass(data) as T & InstanceType<typeof MyClass>;
}
then the following was as expected
const myInstance = GeneratedClass.build({ id: 1 });
console.log(myInstance.id, GeneratedClass.someStatic(), myInstance.someMethod());
Full working example
const ClassFactory = <T>() => {
class MyClass {
constructor(data: T) {
for (const key in data) {
if (!data.hasOwnProperty(key)) { continue; }
(this as any)[key] = data[key];
}
}
public static build(data: T): T & InstanceType<typeof MyClass> {
return new MyClass(data) as T & InstanceType<typeof MyClass>;
}
public static someStatic() {
return 2;
}
public someMethod() {
return 3;
}
}
return MyClass as (new (data: T) => T & InstanceType<typeof MyClass>) & typeof MyClass;
}
const GeneratedClass = ClassFactory<{ id: number }>();
const myInstance = new GeneratedClass({ id: 1 });
console.log(myInstance.id, GeneratedClass.someStatic(), myInstance.someMethod());
const myBuiltInstance = GeneratedClass.build({ id: 1 });
console.log(myBuiltInstance.id, GeneratedClass.someStatic(), myBuiltInstance.someMethod());
Upvotes: 4