Search code examples
typescriptenumsmoduleextending

Extending an imported enum in another file and using it


I'm trying to extend an imported enum in a different file, and using that extended enum in another different file.

The general case

base.enum.ts

export enum MyEnum {
    a = "Foo"
}

extended.enum.ts

import { MyEnum } from './base.enum';

declare module './base.enum' {
    export enum MyEnum {
        b = "Bar"
    }
}

Using in index.ts

import { MyEnum } from './base.enum';
import './extended.enum'; // side-effects import (no usage of export)

console.log(MyEnum.a); // prints "Foo" as expected
console.log(MyEnum.b); // prints undefined, instead of the expected "Bar"

(I'm doing this in TypeScript 2.4.2 which supports string value enums)

I've used this and this SO questions as reference and read the following TypeScript issue in GitHub and still couldn't find a solution to my problem.


Example of usage which resembles mine

Base enum AnimalTypes in animals/base/animal-types.enum.ts:

export enum AnimalTypes { }

Base interface Animal in animals/base/animal.ts:

import { AnimalTypes } from './animal-types';
export interface Animal {
    type: AnimalTypes;
}

animals/base/index.ts:

export * from './animal-types.enum';
export * from './animal';

Extended enum AnimalTypes in animals/animal-types.enum.ts:

import { AnimalTypes } from './base/';
declare module './base/animal-types.enum' {
    export enum AnimalTypes {
        Cat = "cat",
        Dog = "dog"/*,
        ...*/
    }
}

Concrete class Cat in animals/cat.ts:

import { Animal, AnimalTypes } from './base/';
import './animal-types.enum';
export class Cat implements Animal {
    public type: AnimalTypes = AnimalTypes.Cat; // Usage of AnimalTypes with extended value
}

Concrete class Dog in animals/dog.ts:

import { Animal, AnimalTypes } from './base/';
import './animal-types.enum';
export class Dog implements Animal {
    public type: AnimalTypes = AnimalTypes.Dog; // Usage of AnimalTypes with extended value
}

animals/index.ts:

export * from './cat';
export * from './dog';
//...

Final usage in animals/animals-manager.ts:

import { Animal, AnimalTypes} from './base/';
import { Cat, Dog/*, ...*/ } from '.';
import './animal-types'; // side-effects import (no usage of export)

export class AnimalsManager {
    public animals: { [animal: string]: Animal } = {}; // Animal dictionary (I would use AnimalTypes as key type but that isn't supported yet as far as I know).
    constructor() {
        this.animals[AnimalTypes.Cat] = new Cat();
        this.animals[AnimalTypes.Dog] = new Dog();
    }
    //...
}

When trying to use AnimalManager's animals[AnimalTypes.Cat] I'll get the value of animals[AnimalTypes.Dog] because both AnimalTypes.Cat and AnimalTypes.Dog return undefined (which means animals[AnimalTypes.Cat] was overridden by setting animals[AnimalTypes.Dog]).


So, is there currently a way to extend imported enums in TypeScript as described above or will I have to hack my way around to get a custom enum which supports such extending? Thanks.


Solution

  • TypeScript allows to extend only type information in declare module, no code is ever executed from these declarations. In fact, having code inside declare module is in general disallowed, for example placing a function with a body there will give An implementation cannot be declared in ambient contexts error.

    So you declared additional member for MyEnum enum type, but that member is not initialized anywhere, so its value is undefined at runtime. To fix that, you can initialize it yourself:

    extended.enum.ts

    import { MyEnum } from './base.enum';
    
    declare module './base.enum' {
        export enum MyEnum {
            b = "Bar"
        }
    }
    
    const myEnumValues = MyEnum as any;
    myEnumValues["b"] = "Bar";
    

    You have to cast MyEnum to any first, because direct initialization is not allowed:

    MyEnum["b"] = "Bar";
    

    does not compile with error

    error TS2540:Cannot assign to 'b' because it is a constant or a read-only property.