Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
263 views
in Technique[技术] by (71.8m points)

Incompatible types when using enum as generic type in Typescript

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:

Type error

I'm not sure what I'm doing wrong or why typescript doesn't accept it as a valid type. Can someone please help?

question from:https://stackoverflow.com/questions/65852194/incompatible-types-when-using-enum-as-generic-type-in-typescript

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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.

Playground link to code


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...