Reputation: 1282
Let's say I have a generic abstract base class and a set of classes that extend the base and provide the generic param.
abstract class BaseClass<T> {
constructor(protected container: T[]) { }
static someStaticFunc(): string {
return 'I\'m the BaseClass';
}
getElement(i: number): T {
return this.container[i]
}
}
class ConcreteClass1 extends BaseClass<number> {
static someStaticFunc(): string {
return 'I\'m ConcreteClass1'
}
addNumber(element: number) {
this.container.push(element);
}
}
class ConcreteClass2 extends BaseClass<string> {
static someStaticFunc(): string {
return 'I\'m ConcreteClass2'
}
addStringTwice(element: string) {
this.container.push(element);
this.container.push(element);
}
}
class ConcreteClass3 extends BaseClass<boolean> {
// Doesn't add anything
}
I define an interface structure where each value must be a typeof BaseClass
interface SetOfBaseClasses {
Class1: typeof BaseClass,
Class2: typeof BaseClass,
Class3: typeof BaseClass
};
I want to create an object of that interface type, and assign the concrete classes to it. And then call each of the static functions on the class and get the expected results at the bottom.
const classes: SetOfBaseClasses = {
Class1: ConcreteClass1,
Class2: ConcreteClass2,
Class3: ConcreteClass3
}
console.log(classes.Class1.someStaticFunc());
console.log(classes.Class2.someStaticFunc());
console.log(classes.Class3.someStaticFunc());
// Expected output:
// I'm ConcreteClass1
// I'm ConcreteClass2
// I'm the BaseClass
However, TypeScript does not see ConcreteClass1
, ConcreteClass2
or ConcreteClass3
as compatible with typeof BaseClass
. I figure this is because of the generic involved with BaseClass. So I also tried:
interface SetOfBaseClasses {
Class1: typeof BaseClass<unknown>,
Class2: typeof BaseClass<unknown>,
Class3: typeof BaseClass<unknown>
};
and even more precisely:
interface SetOfBaseClasses {
Class1: typeof BaseClass<number>,
Class2: typeof BaseClass<string>,
Class3: typeof BaseClass<boolean>
};
In either case, TypeScript does not seem to allow a generic parameter after typeof Class
I get two errors for each line in the interface that are something like this:
Call signature, which lacks return-type annotation, implicitly has an 'any' return type.(7020)
Type parameter name cannot be 'number'.(2368)
Is there anyway to do this type of thing with TypeScript? Everything seems to be fine with this approach until you involve generics.
Upvotes: 1
Views: 956
Reputation: 329258
The type typeof BaseClass
is more or less equivalent to this:
type TypeofBaseClass =
(abstract new <T>(container: T[]) => BaseClass<T>) &
{ someStaticFunc(): string };
const testTypeofBaseClass: TypeofBaseClass = BaseClass;
Note that TypeofBaseClass
is not itself generic (there is no type parameter on it); rather, it has an abstract
construct signature which is generic. This is much like a generic function in that the generic type parameter is specified when you call it, not in the declaration itself. A value of typeof BaseClass
(or rather, a concrete generic subclass of it like class ConcreteSubclass<T> extends BaseClass<T>
) can construct a BaseClass<T>
for any T
that the person who writes new ConcreteSubclass
wants. It could be a BaseClass<string>
or a BaseClass<number>
or anything else.
That's not the type you want.
Instead, the type you want here is more like this:
type TypeofConcreteClass<T> =
(new (container: T[]) => BaseClass<T>) &
{ someStaticFunc(): string; };
This is a concrete constructor so the abstract
is removed. But more importantly, the scope of the generic type parameter T
has been moved. You want to be able to specify TypeofConcreteClass<number>
which can only construct BaseClass<number>
instances and not anything else. The person who writes new ConcreteClass1
will never be able to get a BaseClass<string>
out.
It is not currently possible to automatically convert typeof BaseClass
into TypeofConcreteClass<T>
without jumping though ugly hoops. The language does not really have a way of representing the relationship between them programmatically. See "TypeScript how to create a generic type alias for a generic function?" and its answer for more information.
For the above, I'd recommend just using a manually written TypeofConcreteClass<T>
definition. Or you could get part of the way there by doing something like
type TypeofConcreteClass<T> =
Pick<typeof BaseClass, keyof typeof BaseClass> &
(new (container: T[]) => BaseClass<T>);
which programmatically grabs all the static
properties/methods of BaseClass
while manually writing out the construct signature.
Let's verify that this works how you want:
interface SetOfBaseClasses {
Class1: TypeofConcreteClass<number>,
Class2: TypeofConcreteClass<string>,
Class3: TypeofConcreteClass<boolean>
};
function main() {
const classes: SetOfBaseClasses = {
Class1: ConcreteClass1,
Class2: ConcreteClass2,
Class3: ConcreteClass3
}
console.log(classes.Class1.someStaticFunc());
console.log(classes.Class2.someStaticFunc());
console.log(classes.Class3.someStaticFunc());
console.log(new classes.Class1([1, 2, 3]).getElement(2).toFixed(3)) // "3.000"
console.log(new classes.Class2(["a", "b", "c"]).getElement(2).toUpperCase()) // "C"
}
That compiles with no error and works as desired.
Upvotes: 1