创建可扩展枚举以在可扩展接口中使用



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

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

目标:下面的伪代码说明了我希望我的模式是什么样子的。

1) 定义基本enum States

2) 使用其他状态扩展enum States,从而导致enum ExtendedStates

2) 使用States和类型化状态机定义ParentInterface

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

4)在class Parent实施ParentInterface

5)扩大class Child实施ChildInterfaceclass Parent

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中大量代码重复。它还丢失了接口,这不是必需的,但确实提供了一个很好的安全网。

我很想听听你们怎么说。我觉得这是一个强大的模式,为了实现它,我错过了一些简单的东西。

它不编译的一个原因是Child不是Parent的正确子类型。 Liskov 替换原理说你应该能够使用Child对象作为Parent对象。 如果我问一个Parent对象它的状态机处于哪种状态,它告诉我ExtendedState,那么我有一个坏Parent,对吧? 所以Child是一个坏Parent,这很糟糕,也是TypeScript警告你的。

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

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);

希望有帮助;祝你好运!

您可以使用 x-extensible-enum。有关说明,您可以获得以下方面的帮助: https://opensource.zalando.com/restful-api-guidelines/#112

最新更新