简体   繁体   中英

typeof class with generic parameter

I'm designing a small API. In it, developers define a class, and a configuration file that allows them to use it with my system.

Below is a simplification of my code:

abstract class Box<T> {
    constructor(protected initialValue: T) {}

    public abstract getDefaultThing(): T;
}

type BoxType = typeof Box;

interface Config {
    boxType: BoxType;
    initialValue: any;
}

function loadConfig(config: Config) {
    // Need to "as any" this so TSC doesn't complain about abstract class
    return new (config.boxType as any)(config.initialValue);
}

Here I define abstract class Box with generic parameter T . I also provide an interface for writing their configuration object that wants a reference to the instantiable Box class, as well as an initialValue. Finally a small implementation function that loads the config by making a new instance of the Box with the value initialValue .

A simple use of this would look like this:

class MyBox extends Box<string> {
    public getDefaultThing(): any {
        return `${this.initialValue} world`;
    }
}

const config: Config = {
    boxType: MyBox,
    initialValue: "hello",
};

const loaded = loadConfig(config);

console.log(loaded.getDefaultThing()); // prints "hello world"

This already doesn't work - boxType in config has an error because MyBox is more narrow than Box . That's fine, what I'd really like to do is redefine my Config interface to type-check initialValue and BoxType :

type BoxType<T> = typeof Box<T>;

interface Config<T> {
    boxType: BoxType<T>;
    initialValue: T;
}

This doesn't work... at all... Box is already a type, and can't accept generics without being instantiated. I know on some level I'm mixing values and types, but is there any way to have a property be a reference to an instantiable generic class narrowed to a certain generic type?

This all works out nicely if you don't use typeof Box but instead type BoxType<T> as a constructor signature that accepts T and returns a Box<T>

abstract class Box<T> {
    constructor(public initialValue: T) {}

    public abstract getDefaultThing(): T;
}

type BoxType<T> = new (initialValue: T) => Box<T>;

interface Config<T> {
    boxType: BoxType<T>;
    initialValue: any;
}

function loadConfig<T>(config: Config<T>) {
    return new config.boxType(config.initialValue); // no cast 
}

class MyBox extends Box<string> {
  public getDefaultThing(): string { // there was a typo here this was any
      return `${this.initialValue} world`;
  }
}

const config = { // no explcit type needed
  boxType: MyBox,
  initialValue: "hello",
};

const loaded = loadConfig(config); // loaded is Box<string>

console.log(loaded.getDefaultThing()); // prints "hello world"

If you want loadConfig to return the derived type not just the abstract type (ie MyBox not Box<string> . You need an extra generic parameter:

abstract class Box<T> {
    constructor(public initialValue: T) {}

    public abstract getDefaultThing(): T;
}

type BoxType<T, TBox extends Box<T>> = new (initialValue: T) => TBox;

interface Config<T, TBox extends Box<T>> {
    boxType: BoxType<T, TBox>;
    initialValue: any;
}

function loadConfig<T, TBox extends Box<T>>(config: Config<T, TBox>) {
    return new config.boxType(config.initialValue); // no cast 
}

class MyBox extends Box<string> {
  public getDefaultThing(): string { // there was a typo here this was any
      return `${this.initialValue} world`;
  }
}

const config = { // no explcit type needed
  boxType: MyBox,
  initialValue: "hello",
};

const loaded = loadConfig(config); // loaded is MyBox

console.log(loaded.getDefaultThing()); // prints "hello world"

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