我已经在我的应用程序中将其作为状态机实现了一个多步骤过程,并创建了表示可能的状态转换的类型:
enum ProcessStep {
STEP_1,
STEP_2a,
STEP_2b,
STEP_3
}
type ValidNextStep<Step extends ProcessStep> = {
[ProcessStep.STEP_1]:
| ProcessStep.STEP_2a
| ProcessStep.STEP_2b;
[ProcessStep.STEP_2a]: ProcessStep.STEP_3;
[ProcessStep.STEP_2b]: ProcessStep.STEP_3;
[ProcessStep.STEP_3]: never;
}[Step]
但是我想知道我是否在此图中创建了一个循环,即ProcessStep.STEP_3
是否可以过渡回ProcessStep.STEP_2a
。
如何在类型级别建立这种不变性?似乎很难,因为默认情况下类型别名不允许循环引用。
答案 0 :(得分:1)
哇,我喜欢这个问题。我不确定是否有一种干净或正确的方法可以仅使用类型系统来执行此操作。
这是一种不干净且可能不正确的方法:强制类型系统进行计算以启动每个状态的状态机并运行许多步骤。如果每个可能的结束状态均为never
,则没有循环。否则,要么有周期,要么图形中有一些很长的非循环路径。
想象一下,您有一个对象类型M
扩展了Record<keyof M, keyof M>
,这意味着M
的值也是M
的键。这描述了一个状态机(ValidNextStep
的定义中有这种类型,但是可以通过索引它来破坏它……不用担心,我们可以将其重构为{ [K in ProcessStep]: ValidNextStep<K> }
)。对于K
中的任何键M
,您可以一步计算M[K]
,或者两步计算M[M[K]]
,或者三步计算M[M[M[K]]]
,等等。>
我们可以非常快速地组合这些操作以获取疯狂的步骤:
type TwoSteps<M extends Record<keyof M, keyof M>> = { [K in keyof M]: M[M[K]] };
type FourSteps<M extends Record<keyof M, keyof M>> = TwoSteps<TwoSteps<M>>;
type SixteenSteps<M extends Record<keyof M, keyof M>> = FourSteps<FourSteps<M>>;
type TwoHundredFiftySixSteps<M extends Record<keyof M, keyof M>> = SixteenSteps<
SixteenSteps<M>
>;
在不让编译器大吼大叫的情况下,这是我能得到的。
然后我们可以创建一个见证类型,如果检测到周期(或很长的路径),则会导致编译器错误:
type NoCycles<
N extends never = TwoHundredFiftySixSteps<
{ [K in ProcessStep]: ValidNextStep<K> }
>[ProcessStep]
> = true;
这对您最初对ValidNextStep
的定义很好,但是如果我们将其更改为以下内容:
type ValidNextStep<Step extends ProcessStep> = {
[ProcessStep.STEP_1]: ProcessStep.STEP_2a | ProcessStep.STEP_2b;
[ProcessStep.STEP_2a]: ProcessStep.STEP_3;
[ProcessStep.STEP_2b]: ProcessStep.STEP_3;
[ProcessStep.STEP_3]: ProcessStep.STEP_2a; // oops
}[Step];
然后NoCycles
定义会产生以下错误:
// Type 'ProcessStep.STEP_2a | ProcessStep.STEP_3' does not satisfy the constraint 'never'.
表明在将机器运行256个步骤后,仍然可以进入STEP_2a
或STEP_3
,这表示一个周期。
这是个好主意吗?可能不是。。。不能保证它是正确的,并且可能对编译器造成比应承受的更大的负担。但是我不知道我要努力找到更好的东西。使用通用语言,您会尝试找到some efficient algorithm来检测一个循环,但是TypeScript中的类型系统不太可能表现出足够的表现力来实现它。
使用它需要您自担风险,祝您好运!