Reputation: 8136
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?
Upvotes: 6
Views: 3227
Reputation: 249766
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"
Upvotes: 5