Created
May 18, 2019 14:30
-
-
Save bradleyayers/a9bda811cef2154e7bc0f6c359c3be33 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { SafeAny } from "@dvtl/types"; | |
/** | |
* Create a finite-state-machine (FSM) transitioning function by declaring the | |
* relationships between states. | |
* | |
* The FSM is modeled as a set of states and transitions between those states. | |
* | |
* The returned `transition` function is statically typed to ensure only valid | |
* transitions between states can occur. | |
* | |
* Example: | |
* | |
* ```ts | |
* interface StateOne { | |
* type: "one"; | |
* text1: string; | |
* } | |
* | |
* interface StateTwo { | |
* type: "two"; | |
* text2: string; | |
* } | |
* | |
* interface StateThree { | |
* type: "three"; | |
* } | |
* | |
* type State = StateOne | StateTwo | StateThree; | |
* const transition = createTransition<State>()({ | |
* one: { | |
* two: () => ({ type: "two", text: "" }) | |
* }, | |
* two: { | |
* one: () => ({ type: "one", text: "" }), | |
* oneWithSameText: prevState => ({ type: "one", text: prevState.text }), | |
* oneWithAppendedText: (prevState, opts: { suffix: string }) => ({ type: "one", text: prevState.text + opts.suffix }) | |
* }, | |
* three: {} | |
* }); | |
* | |
* // Build a node for state `{ type: "one", pos: "foo" }` | |
* const one = { type: "one", text: "foo" }); | |
* | |
* // Transition to state `two`. | |
* const two = transition(one).two(); | |
* | |
* // Transition to state `one`, keeping the state `text` value but with a string appended. | |
* transition(two).oneWithAppendedText({ text: "…" }); | |
* ``` | |
* | |
* It is required that the object passed in actually has the `type` property of | |
* type `string`. | |
*/ | |
export function createTransition<StateShape extends IState>() { | |
return function<Transitions extends ITransitionImpl<StateShape>>(transitions: Transitions) { | |
return function transition<CurrentStateShape extends StateShape>( | |
state: CurrentStateShape, | |
): CurrentStateShape extends StateShape ? Transition<StateShape, CurrentStateShape, Transitions> : never { | |
const { type } = state; | |
const to = (transitions as SafeAny)[type]; | |
return typeof to !== "object" | |
? undefined | |
: (Object.keys(to) | |
.map(key => [key, to[key]]) | |
.reduce((accum, [key, value]) => ({ ...accum, [key]: (opts: SafeAny) => value(state, opts) }), {}) as SafeAny); | |
}; | |
}; | |
} | |
export type IStateType = string; | |
export type IState = { type: IStateType }; | |
export type ITransitionImpl<State> = { | |
[StateTypeFrom in StateTypeFromState<State>]: { | |
[TransitionLabel: string]: (prevState: StateByType<State, StateTypeFrom>, opts: SafeAny) => State; | |
} | |
}; | |
export type StateByType<State, StateType extends IStateType> = State extends { type: StateType } ? State : never; | |
export type StateTypeFromState<State> = State extends { type: infer T } ? (T extends IStateType ? T : never) : never; | |
export type Transition< | |
State extends IState, | |
CurrentStateShape extends State, | |
Transitions extends ITransitionImpl<State> | |
> = Transitions extends { [key in CurrentStateShape["type"]]: infer NextTransitions } | |
? { | |
[TransitionLabel in keyof NextTransitions]: NextTransitions[TransitionLabel] extends () => infer NextStateShape | |
? NextStateShape extends State | |
? () => NextStateShape | |
: never | |
: NextTransitions[TransitionLabel] extends (prevState: CurrentStateShape) => infer NextStateShape | |
? NextStateShape extends State | |
? () => NextStateShape | |
: never | |
: NextTransitions[TransitionLabel] extends (prevState: CurrentStateShape, opts: infer Opts) => infer NextStateShape | |
? NextStateShape extends State | |
? (opts: Opts) => NextStateShape | |
: never | |
: never | |
} | |
: never; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment