创建可扩展的枚举以用于可扩展接口

时间:2017-09-03 16:24:43

标签: typescript enums typescript-typings

上下文:我正在尝试使用TypeState库开发用于在typescript中创建可扩展状态机的模式。 TypeState为Typescript提供了一个类型安全的状态机,虽然不是问题的核心,但我有这个帮助说明我的目标。

问题:我遇到了创建可扩展模式的问题,以便在Typescript中扩展enum并在interfaceclass声明中实现它们。

目标:下面的伪代码说明了我希望我的模式看起来像什么。

1)定义基础enum States

2)使用其他状态扩展enum States,结果为enum ExtendedStates

2)使用ParentInterface和键入状态机

定义States

3)通过ParentInterface扩展ChildInterface并使用States覆盖ExtendedStates

4)在ParentInterface

中实施class Parent

5)在实施class Parent

class Child中扩展ChildInterface

6)能够从任一班级调用broadcastState()并获得当前状态。

我已经在其他语言中使用了这种模式,并且我非常感谢帮助理解Typescript的限制以及可以实现相同目标的任何替代模式。

import {TypeState} from "typestate";

enum States {
  InitialState
}

// extends is not available on enum, looking for alternative
enum ExtendedStates extends States {
  AdditionalState
}

/////////////////////////////////////////
// this works fine
interface ParentInterface {
  fsm: TypeState.FiniteStateMachine<States>;
  states: typeof States;
  message: string;
}

// incorrectly extends ParentInterface, types of fsm/states are incompatible
interface ChildInterface extends ParentInterface {
  fsm: TypeState.FiniteStateMachine<ExtendedStates>;
  states: typeof ExtendedStates;
}

/////////////////////////////////////////

class Parent implements ParentInterface {
  public fsm: TypeState.FiniteStateMachine<States>;
  public states: typeof States;
  public message: string = "The current state is: ";

  constructor(state: States | undefined) {
    state = state ? state : this.states.InitialState;
    this.fsm = new TypeState.FiniteStateMachine(state);
    this.broadcastCurrentState();
  }

  public broadcastCurrentState(): void {
    console.log(this.message + this.fsm.currentState);
  }
}

class Child extends Parent implements ChildInterface {
  public fsm: TypeState.FiniteStateMachine<ExtendedStates>;
  public states: typeof ExtendedStates;

  constructor(state: ExtendedStates | undefined) {
    state = state ? state : this.states.InitialState;
    this.fsm = new TypeState.FiniteStateMachine(ExtendedStates);
    this.broadcastCurrentState();
  }
}

最近我已经

import {TypeState} from "typestate";

enum States {
  InitialState
}

enum ExtendedStates {
  InitialState,
  ExtendedState
}

class Parent {
  public fsm: TypeState.FiniteStateMachine<States>;
  public states: typeof States;
  public message: string = "The current state is: ";

  // T is declared but never used
  constructor(state: <T> | undefined) {
    state = state ? state : this.states.InitialState;
    // cannot find name T
    this.fsm = new TypeState.FiniteStateMachine<T>(state);
    this.broadcastCurrentState();
  }

  public broadcastCurrentState(): void {
    console.log(this.message + this.fsm.currentState);
  }
}

// types of fsm are incompatible
class Child extends Parent {
  public fsm: TypeState.FiniteStateMachine<ExtendedStates>;
  public states: typeof ExtendedStates;

  constructor(state: ExtendedStates | undefined) {
    // Param not assignable to type <T>
    super(state);
  }
}

此尝试接近期望的结果,但不编译并导致enum中的大量代码重复。它也失去了接口,这不是一个要求,但提供了一个很好的安全网。

我很想听听你们所说的话。我觉得这是一个强大的模式,我为了实现它而缺少一些简单的东西。

1 个答案:

答案 0 :(得分:2)

无法编译的一个原因是ul { display: block; font-size: 30px; margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0; font-weight: bold; color: red; } 不是Child的正确子类型。 Liskov substitution principle表示您应该能够将Parent对象用作Child对象。如果我问一个Parent对象状态机所处的状态,它告诉我Parent,那么我有一个损坏的ExtendedState,对吧?因此,Parent是一个损坏的Child,这是不好的,也是TypeScript警告你的。

可能最好忘记拥有超类/子类关系并且只有一个泛型类:

Parent

现在,如果class Generic<T extends States> { public fsm: TypeState.FiniteStateMachine<T>; public states: T; public message: string = "The current state is: "; // T[keyof T] means the values of T, in this case InitialState, etc constructor(state: T[keyof T] | undefined) { state = state ? state : this.states.InitialState; // cannot find name T this.fsm = new TypeState.FiniteStateMachine<T>(state); this.broadcastCurrentState(); } public broadcastCurrentState(): void { console.log(this.message + this.fsm.currentState); } } 是正确的对象,那么工作,但正如您所指出的那样,States并不是真正具有足够功能的enum以这种方式使用:你无法得到任何东西来扩展它们。因此,不使用enum,为什么不使用模拟它的对象:

// make our own enum
type Enum<T extends string> = {[K in T]: K};

// create an enum from given values
function makeEnum<T extends string>(...vals: T[]): Enum<T> {
  const ret = {} as Enum<T>;
  vals.forEach(k => ret[k] = k)
  return ret;
}

// take an existing enum and extend it with more values
function extendEnum<T extends string, U extends string>(
  firstEnum: Enum<T>, ...vals: U[]): Enum<T | U> {
    return Object.assign(makeEnum(...vals), firstEnum) as any;  
}

在这种情况下,Enum<>是一个具有指定字符串键的对象,其值与键相同(这与值为数字的常规enum略有不同。如果您我真的想要那些可以安排的数字,但实现起来会比较烦人。我从来没有使用过TypeState库,所以我不知道它是否关心数值或字符串。)现在你可以像这样创建StatesExtendedStates

const States = makeEnum('InitialState'); 
type States = typeof States; 
// States is { InitialState: 'InitialState' };

const ExtendedStates = extendEnum(States, 'ExtendedState');
type ExtendedStates = typeof ExtendedStates;
// ExtendedStates is { InitialState: 'InitialState', ExtendedState: 'ExtendedState' };

并创建如下对象:

const parentThing = new Generic<States>(States.InitialState);
const childThing = new Generic<ExtendedStates>(ExtendedStates.InitialState);

希望有所帮助;祝你好运!