33

I want to define a typescript interface to represent, say, an error. Something like this:

enum MessageLevel {
    Unknown,
    Fatal,
    Critical,
    Error,
    Warning,
    Info,
    Debug
}

interface IMyMessage {
    name: string;
    level: MessageLevel;
    message: string;
}

This works fine as far as it goes. However, now (perhaps) I want to declare that interface in a .d.ts file so others can use it for typing. But I don't want to define the enum in the .d.ts file, since that would be implementation and not simple typing information. The enum should presumably be in a .ts file, let's call it messageLevel.ts:

///<amd-module name='MessageLevel'/>

export enum MessageLevel {
    Unknown,
    Fatal,
    Critical,
    Error,
    Warning,
    Info,
    Debug
}

and I can, at this point, use it in my d.ts typing file this way:

import * as ml from "./MessageLevel";

interface IMyMessage {
    name: string;
    level: ml.MessageLevel;
    message: string;
}

and I can make this work, but I don't like the level-mixing of importing an implementation file into a typing file. Nor do I like the idea of actually implementing an enum in a typings file.

Is there a clean way to do this that keeps implementation and declaration strictly separate?

Stephan G
  • 3,289
  • 4
  • 30
  • 49

3 Answers3

20

The best solution may depend on whether you have a preference for the actual JavaScript variable being a number, a string, or otherwise. If you don't mind String, you can do it like this:

///messagelevel.d.ts
export type MessageLevel = "Unknown" | "Fatal" | "Critical" | "Error";



///main.d.ts
import * as ml from "./MessageLevel";

interface IMyMessage {
    name: string;
    level: ml.MessageLevel;
    message: string;
}

So in the end JavaScript, it will simply be represented as a string, but TypeScript will flag an error anytime you compare it to a value not in that list, or try to assign it to a different string. Since this is the closest that JavaScript itself has to any kind of enum (eg, document.createElement("video") rather than document.createElement(ElementTypes.VIDEO), it might be one of the better ways of expressing this logic.

Katana314
  • 8,429
  • 2
  • 28
  • 36
  • Thanks very much for your insights.... but the real point of my question is not whether to store virtual enums as strings or as numbers or as types, but how to keep the implementation of the enum out of the typing file. It seems counter to the spirit of TypeScript to be importing typing files in any regard. And if dealing with AMD, doubly dis-spiriting... – Stephan G Jul 14 '16 at 22:34
  • @StephanGolux There is no actual *implementation* code in my sample above, so I'm not sure what you mean. Both my samples are .d.ts files. Having one typing file reference another is extremely common and logical for this scenario - you want one type (an `enum`, a `type`, a `class`, whatever) to be referenced in two files. To try to understand what you intend, could you give me an example of how you would like this to look in pseudocode (ignoring whether TypeScript compiles it?) – Katana314 Jul 15 '16 at 13:38
  • Many thanks, @Katana314. Clearly there is something I am missing on a fundamental conceptual level. I want to implement an enum in a .ts file, presumably using "export enum MyEnum {...} so that I can import that implementation in other .ts files. That is the behavior I want. However, I want to specify a property of an interface in a .d.ts file to be typed as that enum. I don't think it would be proper to import from .ts file into a .d.ts file. Not sure what pseudocode I might offer that would clarify my original question further, but please let me know if you have an idea. – Stephan G Jul 15 '16 at 19:19
  • 1
    @StephanGolux Typescript files compile to JS, but .d.ts files should not result in any JS - they only help TypeScript files enforce correctness. If you wanted to make a strict enum, which would then be accessed by TS or JS as "MyEnum.Thing", then that would mean making an actual JavaScript object; so a variable/constant, not an ethereal "type". String-type restrictions (above) are a good approximation because a JavaScript file would not need access to the same code, and TypeScript will still require you to spell AppliedPhlebotinumAccessorThing correctly. Does that help? – Katana314 Jul 16 '16 at 19:55
  • 1
    Dear @Katana314, yes I understand this exactly. But here is the issue. I want an interface in a .d.ts file, to enforce correctness, as you say. And I want that interface to have a property which is an enum. As you say, that enum can't be implemented in the .d.ts file, it needs to be implemented in a .ts file. But if I import a .ts file into a .d.ts file, then I am crossing a boundary of declaration... then my .d.ts file will be pulling in implementation details Are you suggesting this is what the designers of Typescript intend? This seems really messy. – Stephan G Jul 18 '16 at 16:44
  • 1
    @StephanGolux It is messy, which is why JavaScript, and TypeScript by extension, have preferred a different route. It's common to think "I don't want strings - that's a very loose type" but think of it this way: Another way of expressing `MyEnum.ThingChoiceFive` is `MyEnum["ThingChoiceFive"]` (just due to the way JavaScript does modules/objects). Let's say you were working in JavaScript; you could get that spelling or type wrong in the same ways and have the same result as a bad TypeScript compile. Since tokenized strings are at work anyway, TypeScript's design preference cuts the middleman. – Katana314 Jul 18 '16 at 19:51
  • So, @Katana314, are you suggesting then that the best solution is never to specify an interface in a typing file that has a property of type enum? So all properties of type enum would only be defined in classes and not interfaces? This gets rid of the messiness, of course, but reduces some conceptual power and expressiveness. Best one can do? – Stephan G Jul 18 '16 at 21:10
  • 1
    @StephanGolux My suggestion is to not lock yourself into using an *enum*. Enums themselves are implementation code, not type definitions (you cannot refer to one without the object made). They're also unintuitive to use if ever referred to in raw JavaScript, and may require one more import than really necessary. My preference is for the `type` keyword as shown in my answer. – Katana314 Jul 18 '16 at 21:18
  • Yes this seems correct. We have some places where we really want enums, and I think we will use them without "public" typing exposure by referencing them in classes rather than interfaces. But your solution will work for some other important circumstances. – Stephan G Jul 19 '16 at 14:38
15

I was thinking about this issue these last couple of days, and perhaps a const enum, coupled with union types, may be a suitable option.

This approach depends on the fact that your API clients can expect some enum that is not explicitly declared in your API files.

Consider this. First, the API file api.d.ts:

/**
 * @file api.d.ts
 * 
 * Here you define your public interface, to be
 * implemented by one or more modules.
 */


/**
 * An example enum.
 *  
 * The enum here is `const` so that any reference to its
 * elements are inlined, thereby guaranteeing that none of
 * its members are computed, and that no corresponding 
 * JavaScript code is emmitted by the compiler for this
 * type definition file.
 * 
 * Note how this enum is named distinctly from its
 * "conceptual" implementation, `MyEnum`.
 * TypeScript only allows namespace merging for enums
 * in the case where all namespaces are declared in the
 * same file. Because of that, we cannot augment an enum's
 * namespace across different source files (including
 * `.d.ts` files).
 */
export const enum IMyEnum { A }

/**
 * An example interface.
 */
export interface MyInterface {

    /**
     * An example method.
     * 
     * The method itself receives `IMyEnum` only. Unfortunately,
     * there's no way I'm aware of that would allow a forward
     * declaration of `MyEnum`, like one would do in e.g. C++
     * (e.g. declaration vs definition, ODR).
     */
    myMethod(option: IMyEnum): void;
}

And an API implementation, impl.ts:

/**
 * @file impl.ts
 */

/**
 * A runtime "conceptual" implementation for `IMyEnum`.
 */
enum MyEnum {
    // We need to redeclare every member of `IMyEnum`
    // in `MyEnum`, so that the values for each equally named
    // element in both enums are the same.
    // TypeScript will emit something that is accessible at
    // runtime, for example:
    //
    //    MyEnum[MyEnum["A"] = 100] = "A";
    //
    A = IMyEnum.A
}

class MyObject implements IMyInterface {

    // Notice how this union-typed argument still matches its
    // counterpart in `IMyInterface.myMethod`.
    myMethod(option: MyEnum | IMyEnum): void {
        console.log("You selected: " + MyEnum[option]);
    }
}

// ----

var o = new MyObject();
o.myMethod(MyEnum.A);  // ==> You selected: 100
o.myMethod(IMyEnum.A); // ==> You selected: 100

// YAY! (But all this work shouldn't really be necessary, if TypeScript
// was a bit more reasonable regarding enums and type declaration files...)

I made this gist as an example, in case someone would like to see this approach in action.

Flávio Lisbôa
  • 651
  • 5
  • 19
6

Almost two years later, this problem still exists. I could not find a good solution so I created a workaround, which tells your interface only that the type of the var is an enum, but not which enum. There's a "middleware" abstract wrapper for your main class which concretely sets the var type to be the needed enum.

// globals.d.ts

type EnumType = { [s: any]: any }

interface IMyMessage {
  level: EnumType
}
// enums.ts

export enum MessageLevel {
    Unknown,
    Fatal,
    Critical,
    Error,
    Warning,
    Info,
    Debug
}
// MyClass.ts

import { MessageLevel } from 'enums'

// If your MyMessage class is extending something, MyMessageWrapper has to 
//  extend it instead!
abstract class MyMessageWrapper extends X implements IMyMessage {
  abstract level: MessageLevel
}

class MyMessage extends MyMessageWrapper {
  level = MessageLevel.Unknown // works
  // level = MyOtherEnum.Unknown // doesn't work
}

Might be useful in some use cases.

Damian Dobrev
  • 61
  • 1
  • 3