【问题标题】:How do I require that an enum has certain members我如何要求枚举具有某些成员
【发布时间】:2021-08-30 18:01:48
【问题描述】:

我正在设计一个涉及一些有限状态机的 lib API,因此假设该 lib 导出以下接口:

export interface FSM<TStates> {
  state: TStates
  // ... other properties
}

该库要求状态机具有状态'started''finished'。我一直在尝试将这个约束编码到类型系统中,但没有取得多大成功。

到目前为止,我已经尝试将此约束实现为枚举:

export enum BaseState {
  STARTED = 'started',
  FINISHED = 'finished',
}

export interface FSM<TStates extends BaseState> {
  state: TStates
  // ... other properties
}

enum MyState {
  STARTED = 'started',
  OTHER = 'other',
  FINISHED = 'finished',
}

// Type 'MyState' does not satisfy the constraint 'BaseState'.ts(2344)
let fsm: Fsm<MyState>

我尝试了联合类型

export type BaseState = 'started' | 'finished';

export interface FSM<TStates extends BaseState> {
  state: TStates
  // ... other properties
}

type MyState = 'started' | 'finished' | 'other'

// Type 'MyState' does not satisfy the constraint 'BaseState'.
//   Type '"other"' is not assignable to type 'BaseState'.ts(2344)
let fsm: Fsm<MyState>

是否可以在打字稿中表示这种约束?

【问题讨论】:

  • 特别是您的联合示例,FSM 的定义声明“我在'开始'和'完成'状态下工作”。这就是为什么传入MyState 不起作用的原因——FSM 没有声明它了解如何使用other 状态。我在过去实现您想要的模式的一种方法是创建state 字段state: BaseState &amp; TStates,然后重构TStates 以表示BaseStates 之上的所有其他状态。

标签: typescript enums type-safety


【解决方案1】:

UPD:顺便说一句,如果你能做到,并不意味着你应该这样做。也许 cmets 中重新定义问题并将BaseState &amp; TStates 用于state 的建议是一个更好的建议。尽管在这种情况下您无法使用这些属性定义自己的枚举。

不管怎样

首先是工会。正如您已经了解的那样,您不能将TStates 类型参数限制为扩展'started' | 'finished',因为那样您将无法添加额外的字段。但是,您可以要求 'started' | 'finished' 扩展 TStates。一种方法是:

// Allow TStates to be any string
export type FSM<TStates extends string> =
  'started' | 'finished' extends TStates ?
    {
      state: TStates
    } :
    never

如果'started' | 'finished' 不扩展TStates,那么您将无法为FSM&lt;TStates&gt; 类型的变量分配任何值。您还可以检查TStates 本身不是string,这样您就不能只使用FSM&lt;string&gt;

export type FSM<TStates extends string> =
  'started' | 'finished' extends TStates ?
  string extends TStates ?
    never :
    {
      state: TStates
    } :
    never

这将只接受字符串联合,因为目前无法创建像Exclude&lt;string, 'foo'&gt; 这样的类型。如果是TStates extends string,那么它要么是string 本身,要么是字符串文字的联合。

这看起来有点难看,但也许实用程序类型会有所帮助

type IfIncludesStartedFinished<TStates extends string, T> = 
  'started' | 'finished' extends TStates ? 
    string extends TStates ? never : T : never

export type FSM<TStates extends string> = IfIncludesStartedFinished<
  TStates,
  {
    state: TStates
  }
>

尽管您应该小心never,因为never 类型的变量可以分配给任何其他变量。也许使用不透明的错误类型会有所帮助

declare const errorTypeSymbol: unique symbol
type ErrorType<TMessage extends string> = TMessage & {error: errorTypeSymbol}

type IfIncludesStartedFinished<TStates extends string, T> = 
  'started' | 'finished' extends TStates ?
    string extends TStates ? 
      ErrorType<"TStates must be a union of strings, not a string"> :
      T :
      ErrorType<"TStates must include started and finished">

现在,如果出现问题,当您将鼠标悬停在错误上时,您会在打字稿投诉之间的某处看到错误消息。虽然你还是要小心。


现在说到枚举,它有点 hacky。打字稿中字符串枚举背后的想法是,如果您更改 1 的值,代码不会注意到它并以相同的方式工作。如果您要求枚举包含一些值,则不再是这种情况,因此也许您应该考虑不这样做。但我不认为这是不可饶恕的罪过,如果你愿意,你完全可以不在乎。

最近在 ts 中,可以使用模板文字类型将枚举值的类型作为字符串的联合来获取,例如 `${Enum}`(天哪,当 stackoverflow 事物是 markdown 的一部分时,如何在内联代码中包含反引号并不明显)。您可以利用它来稍微改变我上面描述的类型

export type FSM<TStates extends string> = IfIncludesStartedFinished<
  // In case TStates is a enum, turn it into a union of strings
  `${TStates}`,
  {
    state: TStates
  }
>

顺便说一句,这将允许您对TStates 使用字符串联合和枚举

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-05
    • 2016-09-08
    • 1970-01-01
    相关资源
    最近更新 更多