Eduardo Rosostolato
Eduardo Rosostolato

Reputation: 858

Typescript: Class extends a Generic Type

I know it's too generic, but I wish to make a class that would have all props and prototype from a generic type like this:

class GenericExtend<T> extends T {
    constructor(data: T) {
        // here is a workaround to make these 2 classes unique
        const proto = { ...GenericExtend.prototype };
        Object.assign(proto, Object.getPrototypeOf(data));
        Object.setPrototypeOf(this, proto);
        Object.assign(this, data);
    }

    GenericMethod() { }
}

And now on, I could instanciate GenericExtend class and then get the types of the two classes like this:

const obj = new GenericExtend<Model>(data);
obj.GenericMethod(); // ok
obj.ModelMethod(); // good!

One of my solution is to use intersection, something like this:

const obj: GenericExtend & Model = new GenericExtend(data) as any;

It worked, but I didn't like that much. Is there something better that I can do?

Upvotes: 14

Views: 20042

Answers (2)

Jeff Tian
Jeff Tian

Reputation: 5913

I came accross into the similar requirement and finally made it work in the following way, by which no intersection is strictly needed (you can define a dedicated type for type checking), hope it helps.

class A {
  a() {
    ...
  }
}

class B {
  b() {
    ...
  }
}

type Constructor = new (...args: any[]) => any

function genericExtend<T extends Constructor>(target: T) {
  return class GenericExtended extends target {
    constructor(...args: any[]) {
      super(...args)
    }

    genericMethod() {
      ...
    }
  }
}

const instanceOfA: GenericExtended & A = new (genericExtend(A))()
const instanceOfB = new (genericExtend(B))()

instanceOfA.a() // ok with type checking
instanceOfA.genericMethod() // ok with type checking

instanceOfB.b() // ok without type checking
instanceOfB.genericMethod() // ok without type checking

Upvotes: 2

jcalz
jcalz

Reputation: 330086

TypeScript will not let you implement or extend another type T unless all the keys of T are statically known at compile time. That prevents class GenericExtend<T> implements T {...} from being something you can write.

Instead, you have to use an intersection to get this behavior... but you can confine the type assertion to the constructor function, if you want, so that subsequent uses will not require it. Let's rename GenericExtend out of the way:

class _GenericExtend<T> {
  constructor(data: T) {
    const proto = { ..._GenericExtend.prototype };
    Object.assign(proto, Object.getPrototypeOf(data));
    Object.setPrototypeOf(this, proto);
    Object.assign(this, data);
  }
  GenericMethod() { }
}

And then redefine GenericExtend as both a type and a constructor with the intersection behavior you want:

type GenericExtend<T> = _GenericExtend<T> & T;
const GenericExtend: new <T>(data: T) => GenericExtend<T> = _GenericExtend as any;

That last as any is the type assertion we need. Now you should be able to get the behavior you want:

interface Model {
  ModelMethod(): void;
}
declare const data: Model;

const obj = new GenericExtend(data);
obj.GenericMethod(); // ok
obj.ModelMethod(); // ok

Playground link to code

Upvotes: 17

Related Questions