Reputation: 369
Consider the snippet below. The idea is that I have provider implementations that extend a base provider, and each provider accepts a settings object that extends a base settings object. Each provider has also a static method to test those settings before they are passed to the provider (the method is static due to legacy reasons and right now, making it an instance method is not an option)
enum ProviderType {
TypeA = 'typeA',
TypeB = 'typeB',
}
interface Settings {
commonProperty: string;
}
interface SettingsTypeA extends Settings {
propertyA: string;
}
interface SettingsTypeB extends Settings {
propertyB: string;
}
type SettingsMap = {
[ProviderType.TypeA]: SettingsTypeA,
[ProviderType.TypeB]: SettingsTypeB,
}
interface TestSettingsOptions<T extends ProviderType> {
settings: SettingsMap[T];
}
abstract class BaseProvider<T extends ProviderType> {
constructor(protected settings: SettingsMap[T]) {}
static testSettings<T extends ProviderType>(opts: TestSettingsOptions<T>) {
throw new Error('Method not implemented');
}
}
class ProviderA extends BaseProvider<ProviderType.TypeA> {
constructor(protected settings: SettingsTypeA) {
super(settings); // Settings has the correct type here: SettingsTypeA
}
static testSettings(opts: TestSettingsOptions<ProviderType.TypeA>) {
// do some testing
}
}
class ProviderB extends BaseProvider<ProviderType.TypeB> {
constructor(protected settings: SettingsTypeB) {
super(settings); // Settings has the correct type here: SettingsTypeB
}
static testSettings(opts: TestSettingsOptions<ProviderType.TypeB>) {
// do some testing
}
}
While the basic classes, interfaces and mapped types are inferred correctly, the static methods are having an issue. See the image below from my IDE:
I'm not sure what I'm doing wrong or why typescript doesn't accept it as a valid type. Can someone please help?
Upvotes: 0
Views: 564
Reputation: 328097
As you have encountered, the static side of a class
has no access to any of the instance side's generic type parameters. In some sense it is not generally meaningful to allow such access, because there is a single constructor and multiple instances... a class constructor like class Foo<T> {}
has a type like new() => Foo<T>
; a single constructor needs to be able to create a Foo<T>
for any possible T
. So the constructor itself has no specific T
that it can access.
There is a feature request at microsoft/TypeScript#34665 to allow such access inside the type signature for abstract static
methods, should TypeScript ever get them. Right now, neither abstract static
methods nor static access to instance type parameters are permitted. So you can't do this directly.
The obvious solution here is to make testSettings()
an instance method, but you can't do that for whatever reason.
The next possible way forward is to make a generic factory function which spits out non-generic classes. This is the solution presented in this SO answer. In your case it looks like this:
function BaseProvider<T extends ProviderType>(type: T) {
abstract class BaseProvider {
constructor(protected settings: SettingsMap[T]) {
}
static testSettings?(opts: TestSettingsOptions<T>) {
throw new Error('Method not implemented');
}
}
return BaseProvider;
}
The type parameter T
is in scope everywhere inside the implementation of the BaseProvider
function, including the static side of the locally declared class that gets returned. Note that the type
parameter passed in is used only to help the compiler infer T
; I'm not using the value anywhere. But you could, if you wanted to.
And now your subclasses won't extend BaseProvider
, but the output of BaseProvider
called on whatever enum type you want:
class ProviderA extends BaseProvider(ProviderType.TypeA) {
constructor(protected settings: SettingsTypeA) {
super(settings);
}
static testSettings(opts: TestSettingsOptions<ProviderType.TypeA>) {
// do some testing
}
}
class ProviderB extends BaseProvider(ProviderType.TypeB) {
constructor(protected settings: SettingsTypeB) {
super(settings);
}
static testSettings(opts: TestSettingsOptions<ProviderType.TypeB>) {
// do some testing
}
}
All of that now compiles. Of course there are caveats. The ones I can think of:
something like instanceof BaseProvider
will no longer work; not even instanceof BaseProvider(ProviderType.TypeA)
will work, because there is no unique class constructor anymore.
if you need to export BaseProvider
declarations in a .d.ts file, you'll need to do extra work to annotate types; function-local classes tend not to be exportable cleanly.
Upvotes: 1