Reputation: 2402
In the following code, I've outlined two problems (see comments):
Base
should take BaseOptions
while Sub
should take SubOptions
which are a superset of BaseOptions
).Solutions with extends BaseOptions | SubOptions
don't work for me, as I cannot enumerate all of the possible, more specific types of options to keep the code generic.
Am I approaching this the wrong way altogether or is there a solution to my problems?
interface BaseOptions {
a: string
}
interface SubOptions extends BaseOptions {
a: string
b: string
}
class Base<T extends BaseOptions> {
options: T
constructor(options: T) {
this.options = options;
}
// Problem 1: When TypeScript is concerned, this method always returns an
// instance of "Base", even when calling 'create' on a subclass (that does not
// override 'create'). How can this be fixed?
static create<T extends BaseOptions>(options: T): Base<T> {
return new this(options);
}
}
// Problem 2: Class 'Sub' requires more specific options than class 'Base', but
// TypeScript does not allow me to validate it:
// 2417: Class static side 'typeof Sub'
// incorrectly extends base class static
// side 'typeof Base'.
class Sub<T extends SubOptions> extends Base<T> {
static create<T extends SubOptions>(options: T): Sub<T> {
return new this(options);
}
}
Upvotes: 4
Views: 539
Reputation: 2290
Remove all the unnecessary stuff and keep it real. This works:
type BaseOptions = {
a: string;
};
type SubOptions = {
a: string;
b: string;
} & BaseOptions;
class Base<BO extends BaseOptions> {
options: BO;
constructor(options: BO) {
this.options = options;
}
static create<BO extends BaseOptions>(options: BO): Base<BO> {
return new this(options);
}
}
class Sub<SO extends SubOptions> extends Base<SO> {}
You can force TypeScript to use the correct type like this:
const sub = Sub.create({ a: 'a', b: 'b' }) as Sub<...>;
This might be a TypeScript error - that's it; I really said that. Here is why I think so:
Using the code from the short answer we can create an object called sub
like this:
const sub = Sub.create({ a: 'a', b: 'b' });
TypeScript will choose to type infer Base<{ a: string, b: string }
- I am guessing you want to have the more explicit definition and infer the type Sub<...>
.
To see what the underlying JS-Interpreter does, let's see what happens in node when using plain JavaScript.
class Base {
constructor(options) {
this.options = options
}
static create(options) {
return new this(options)
}
}
class Sub extends Base {}
Sub.create({ a: 'a', b: 'b' })
This outputs
> Sub { options: { a: 'a', b: 'b' } }
There you have it. That's the object with the correct class assigned; perfectly safe to overwrite the TypeScript inferred type.
Upvotes: 1
Reputation: 402
interface IGenericMap<T> {
[x: string]: T;
}
class Base<T extends IGenericMap<string>> {
options: T
constructor(options: T) {
this.options = options;
}
static create<T extends IGenericMap<string>>(options: T): Base<T> {
return new this(options);
}
}
class Sub<T extends IGenericMap<string>> extends Base<T> {
constructor(options: T) {
super(options);
}
}
console.log(Base.create({ a: 'one' }));
console.log(Sub.create({ a: 'one', b: 'two' }));
Now you can use the inheritance properly and the create methods returns the appropirate class.
If use the object mapping instead of BaseOptions and SubOptions inside the IGenericMap you are going to have any key with a generic value by choise. In the example above string and you can change it with any other type: number, Object, any, string | number ...
UPDATE: an example with types:
interface BaseOptions {
a: string
}
interface SubOptions extends BaseOptions {
a: string
b: string
}
class Base<T extends BaseOptions> {
options: T
constructor(options: T) {
this.options = options;
}
static create<T extends BaseOptions>(options: T): Base<T> {
return new this(options);
}
}
class Sub<T extends SubOptions> extends Base<T> {
constructor(options: T) {
super(options);
}
}
console.log(Base.create({ a: 'one' }));
console.log(Sub.create({ a: 'one', b: 'two', c: '' }));
Upvotes: 0