我正在尝试创建一个状态机类型,其中的转换由编译器强制执行。
我想这样定义状态机:
type States = {
state1: {state: 'state1'},
state2: {state: 'state2'}
}
const Transitions: TransitionsType<States> = {
init() {
return {state: 'state1'}
},
state1: {
state2({...anyOtherItems}) {
// processing with anyOtherItems
// maybe return a load of extra stuff here
return {...anyOtherItems, state: 'state2'}
}
},
state2: {
state1({...others}) {
// same deal here
return {state: 'state1'}
}
}
};
transition(transitions, {state: 'state1'}, 'state2');
States 是一种描述机器状态的类型,以及该状态的数据类型。
Transitions 是一个对象,其 from 状态为映射的第一级,而 to 为映射的第二级。每次转换都会调用这些函数,它们将采用与 from 状态相关的类型,并返回与 to 状态相关的类型。
init
功能就是把这一切拉开序幕。
transition
是一个函数,它接受状态机定义、状态对象和 to 状态,并返回一个新的状态对象。
如果转换函数返回具有错误状态的对象,并且使用不允许的状态对象和
to状态调用
transition
函数,我希望得到编译错误。
到目前为止我已经(并且可以完全忽略这一点):
type StatesType<S> = {
[T in keyof S]: {state: T};
};
type TransitionTosType<S extends StatesType<S>, F extends keyof StatesType<S>> = {
[T in keyof S]?: (arg: S[F]) => S[T]
}
type TransitionsType<S extends StatesType<S>> = {
[F in keyof S]: TransitionTosType<S, F>
} & {
init: () => S[keyof S]
}
function transition<S extends StatesType<S>>(transitions: TransitionsType<S>,
state: S[keyof S],
to: keyof S & string): {state: string} {
const fromStateTransitions = transitions[state.state as keyof TransitionsType<S>];
if (fromStateTransitions) {
const toTransition = fromStateTransitions[to];
if (toTransition)
return toTransition(state);
}
throw new TypeError(`no transition from ${state.state} to ${String(to)}`);
}
这会给我带来一个错误
return toTransition(state);
,并且如果我尝试转换到不允许的状态,它不会给我一个错误。
我很乐意接受任何可以接受给定状态和转换的答案(示例中的
States
和Transitions
),并提供一个接受一组转换(例如Transitions
)的函数,一个状态正确类型的上下文(如 States
之类的类型中给出的),以及转换 to 的字符串,如果状态上下文是错误的类型,或者 to 状态是错误的,则会出现编译错误t 从 from 状态的有效转换。
我对接受类型参数的转换函数感到满意。
我也会接受解释为什么不可能作为答案。
我在代码示例中看到的主要问题是,手动指定
transitions
的类型为 TransitionsType<States>
与允许编译器从初始化 对象文字 推断出更具体的内容之间存在冲突。类型 TransitionsType<States>
具有相当明确的 States
类型表示,您需要它才能使 transition(transitions, ⋯)
正确运行。但是,如果您使用该类型,那么编译器不知道状态图的细节以及哪些节点连接到哪个节点。您确实需要使用更窄的类型,如下所示:
const transitions = {
init() {
return { state: 'state1' }
},
state1: {
state2({ ...anyOtherItems }) {
return { ...anyOtherItems, state: 'state2' }
}
},
state2: {
state1({ ...others }) {
return { state: 'state1' }
}
}
} satisfies TransitionsType<States>;
satisfies
运算符来验证该类型的可分配性,并提供一个 context 来推断 transitions
的类型。推断的类型是:
const transitions: {
init(): {
state: "state1";
};
state1: {
state2({ ...anyOtherItems }: {
state: 'state1';
}): {
state: "state2";
};
};
state2: {
state1({ ...others }: {
state: 'state2';
}): {
state: "state1";
};
};
}
但是
transitions
的类型并不明确包含 States
类型。因此,您需要 transition(transitions, ⋯)
才能梳理 transitions
的类型以恢复原始 States
类型,该类型不一定完全存在(想象一个其中一个状态已断开连接的图表)。
可能有一些巧妙的方法可以两全其美,但在我的尝试中,输入
transition()
变得如此复杂,以至于对我来说似乎不值得。
相反,我的方法是接受您需要显式地将两条信息传递给
transition()
:类型级别的 States
类型,以及 transitions
的具体形状。理想情况下,调用看起来像这样
transition<States>(transitions, state, to);
但不幸的是,类型级别的类型传递需要所谓的“部分类型参数推断”,如 microsoft/TypeScript#26272 中所要求的,目前 TypeScript 不支持该功能。例如,请参阅Typescript:在可选的第一个泛型之后推断泛型的类型。解决方法涉及一些重构。最常见的是类型级 currying,因此调用
transition<States>(transitions, state, to)
会返回一个使用 transition<States>()
作为参数调用的函数,而不是 (transitions, state, to)
。
它可能看起来像这样:
function transition<S extends StatesType<S>>() {
function f<T extends TransitionsType<S>, F extends S[keyof S]>(
transitions: T,
state: F,
to: keyof T[F["state"] & keyof T]
): void;
function f(transitions: any,
state: any,
to: any): { state: string } {
const fromStateTransitions = transitions[state.state];
if (fromStateTransitions) {
const toTransition = fromStateTransitions[to];
if (toTransition)
return toTransition(state);
}
throw new TypeError(`no transition from ${state.state} to ${String(to)}`);
}
return f;
}
所以现在当你调用
transition<States>()
时,函数内部 S
的类型就已经确定了,并且可以用来 constrain T
(对应于 transitions
的泛型类型,以及 F
(对应于 state
的泛型类型),因此 to
的类型是 F
和 T
的易于编写的函数。
也就是说,如果
T
是 transitions
的类型,并且 F
是 state
的类型,那么 to
一定是 keyof T[F["state"]]
类型(transitions
属性的键之一)
对应状态F
)。请注意,我必须编写 T[F["state"] & keyof T]
才能让编译器相信 F["state"]
可以作为 T
的键。并没有“直接”写出这是真的,至少对于泛型来说不是这样,并且编译器无法推断出它。 交集让编译器无需担心它。
好吧,让我们尝试一下:const transitionFunc = transition<States>();
transitionFunc(transitions, { state: 'state1' }, 'state2'); // okay
transitionFunc(transitions, { state: 'state1' }, 'state1'); // error
transitionFunc(transitions, { state: 'state2' }, 'state2'); // error
transitionFunc(transitions, { state: 'state2' }, 'state1'); // okay
transitionFunc(transitions, { state: 'state3' }, 'state2'); // error
看起来不错。编译器只允许您传入适当类型的参数,并且如果无法从
to
直接访问您的
state
状态,则会向您发出警告。Playground 代码链接