简体   繁体   中英

Create instance of generic class parameter

I want to create an instance of T in a generic class in Typescript. The following code is something I thought would work, but unfortunately it does not.

type Constructor<T> = { new(...args: any[]): T };
class Bar { }
class Foo<T extends Constructor<T>> {

    constructor( /* some parameters here, but NOT a type creator! */ ) {}

    create(): T {
        return new T();
    }
}
const bar: Bar = new Foo<Bar>().create();

Well there are some other questions/answers here on SO, but all use some kind of type creator that needs to be passed to the generic class/function, like so:

function create<T>(creator: new() => T): T {
    return new creator();
}
const bar: Bar = create<Bar>(Bar);

But my ctor needs to not have something like that. The create function should always stay parameter-less. Is there a way to make this work?

In short: No it's not possible without a reference to a constructible runtime value.

The reason is that TypeScript types only exist at compile time (not at runtime) and can't be used as values. In your example, T is only a type (not a runtime value). This is the JavaScript that your TypeScript program compilation would produce:

 class Bar {} class Foo { constructor() { } create() { return new T(); } } const bar = new Foo().create(); // ^^^^^^^^ // Exception is thrown: ReferenceError: T is not defined

You can see that T is a reference to a runtime value that doesn't exist, and so it throws a ReferenceError exception when the create method is invoked.


Instead, you can accept a constructible object (and its arguments) and return a constructed instance:

TS Playground

type Constructible<
  Params extends readonly any[] = any[],
  T = any,
> = new (...params: Params) => T;

class Foo {
  static create <T extends Constructible>(
    constructible: T,
    ...params: ConstructorParameters<T>
  ): InstanceType<T> {
    return new constructible(...params);
  }
}

class Bar {}

class Baz {
  constructor (readonly param1: string, readonly param2: number) {}
}

const bar = Foo.create(Bar);
const baz = Foo.create(Baz, '2', 2);

You can't create any instance of a class without a runtime reference to that class constructor.

You could pass your class to be created as a value to the creator's constructor, and then generically capture that type's value.

I would make you Constructor non-generic, just to use it as a constraint, and let typescript infer the rest.

type Constructor = { new (...args: any[]): any };

For example:

class Foo<T extends Constructor> {
    constructor(private ctor: T) {}

    create(): InstanceType<T> {
        return new this.ctor();
    }
}

Which lets you do this:

class Bar { }

const foo = new Foo(Bar)
const bar1: Bar = foo.create(); // no parameters
const bar2: Bar = foo.create(); // no parameters

Playground


For bonus points, you can pick up the constructor parameters and make your create() method take them:

    create(...ctorArgs: ConstructorParameters<T>): InstanceType<T> {
        return new this.ctor(...ctorArgs);
    }

Which let's you do:

class Bar {
    constructor(a: number, b: string) {}
}

const foo = new Foo(Bar)
const bar1: Bar = foo.create(1, 'a'); // no parameters
const bar2: Bar = foo.create(2, 'b'); // no parameters

Playground

Or you could put this in the creators constructor instead?

class Foo<T extends Constructor> {
    ctorArgs: ConstructorParameters<T>

    constructor(private ctor: T, ...ctorArgs: ConstructorParameters<T>) {
        this.ctorArgs = ctorArgs
    }

    create(): InstanceType<T> {
        return new this.ctor(...this.ctorArgs);
    }
}

class Bar {
    constructor(a: number, b: string) {}
}

const foo = new Foo(Bar, 1, 'a')
const bar1: Bar = foo.create(); // no parameters
const bar2: Bar = foo.create(); // no parameters

Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM