37

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?

3 Answers 3

25
+50

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.

Sign up to request clarification or add additional context in comments.

9 Comments

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...
@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?
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.
@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.
@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.
|
16

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.

Comments

8

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.