AnonAppDev
AnonAppDev

Reputation: 83

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.

Upvotes: 8

Views: 4878

Answers (2)

Ken Ko
Ken Ko

Reputation: 1537

I came across this old post just now when I was trying to work out how a project at work was managing to do this, but I couldn't do the same in my own project (as problem as the OP).

I'm not sure if this is something now supported in the later version of TypeScript (I'm using 4.7 now), but this is possible using const enums.

Following the same example:

base-enum.ts

export const enum MyEnum {
    a = "Foo"
}

extended-enum.ts

declare module "./base-enum" {
    const enum MyEnum {
        b = "Bar"
    }
}

export {}; // make this a module

main.ts

import { MyEnum } from "./base-enum";

console.log(MyEnum.a);
console.log(MyEnum.b);

Running this correctly outputs:

Foo
Bar

The compiled main.js looks like this (module is set to "commonjs" in tsconfig.json), with the compiler replacing the enum reference with the constant.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
console.log("Foo" /* MyEnum.a */);
console.log("Bar" /* MyEnum.b */);
//# sourceMappingURL=main.js.map

To note the differences to the OPs example specifically:

  • Use a const enum
  • In the extending declaration, don't do export
  • Make sure the file with the extending declaration is a module (i.e. must have an import/export statement)

Upvotes: 0

artem
artem

Reputation: 51659

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.

Upvotes: 3

Related Questions