Как ссылаться на перечисление Typescript в файле d.ts при использовании AMD?

Я хочу определить интерфейс машинописного текста для представления, скажем, ошибки. Что-то вроде этого:

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

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

Это прекрасно работает, насколько это возможно. Однако теперь (возможно) я хочу объявить этот интерфейс в файле.d.ts, чтобы другие могли использовать его для набора текста. Но я не хочу определять перечисление в файле.d.ts, так как это будет реализация, а не простая информация о наборе. Предположительно, перечисление должно быть в файле.ts, назовем его messageLevel.ts:

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

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

и теперь я могу использовать его в моем файле d.ts для печати таким образом:

import * as ml from "./MessageLevel";

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

и я могу сделать эту работу, но мне не нравится смешивание уровней при импорте файла реализации в файл ввода. Также мне не нравится идея реализации enum в файле для ввода.

Есть ли чистый способ сделать это, который хранит реализацию и объявление строго раздельно?

2 ответа

Решение

Наилучшее решение может зависеть от того, предпочитаете ли вы, чтобы фактическая переменная JavaScript была числом, строкой или иным образом. Если вы не возражаете против String, вы можете сделать это так:

///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;
}

Таким образом, в конце JavaScript он будет просто представлен в виде строки, но TypeScript сообщит об ошибке каждый раз, когда вы сравниваете ее со значением, отсутствующим в этом списке, или пытаетесь присвоить его другой строке. Так как это самое близкое, что сам JavaScript имеет к любому виду перечисления (например, document.createElement("video") скорее, чем document.createElement(ElementTypes.VIDEO)Это может быть один из лучших способов выразить эту логику.

Я думал об этой проблеме в последние пару дней, и, возможно, const enum в сочетании с типами соединений может быть подходящим вариантом.

Этот подход зависит от того факта, что ваши клиенты API могут ожидать некоторого перечисления, которое явно не объявлено в ваших файлах API.

Учти это. Во-первых, файл API 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;
}

И реализация API, 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...)

Я сделал эту суть в качестве примера на случай, если кто-то захочет увидеть этот подход в действии.

Спустя почти два года эта проблема все еще существует. Мне не удалось найти хорошее решение, поэтому я создал обходной путь, который сообщает вашему интерфейсу только, что тип var является перечислением, но не каким перечислением. Для вашего основного класса существует абстрактная оболочка "промежуточного программного обеспечения", которая конкретно устанавливает тип var как необходимое перечисление.

// 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
}

Может быть полезно в некоторых случаях использования.

Другие вопросы по тегам