Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to refer to Typescript enum in d.ts file, when using AMD?

Tags:

typescript

amd

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?

like image 665
Stephan G Avatar asked Jul 12 '16 17:07

Stephan G


3 Answers

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.

like image 181
Katana314 Avatar answered Nov 09 '22 04:11

Katana314


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.

like image 42
Flávio Lisbôa Avatar answered Nov 09 '22 03:11

Flávio Lisbôa


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.

like image 37
Damian Dobrev Avatar answered Nov 09 '22 02:11

Damian Dobrev