Jason Athanasoglou
Jason Athanasoglou

Reputation: 139

Typescript - Class instance which implements generic type

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

  1. myInstance typings doesn't have the keys of T - I'm expecting myInstance.id to be a number
  2. I have to cast this as any in the constructor to assign the values by the given data

In 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

Answers (2)

Julien
Julien

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

Jason Athanasoglou
Jason Athanasoglou

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

Related Questions