Skip to content

Instantly share code, notes, and snippets.

@bradleyayers
Created May 18, 2019 14:30
Show Gist options
  • Save bradleyayers/a9bda811cef2154e7bc0f6c359c3be33 to your computer and use it in GitHub Desktop.
Save bradleyayers/a9bda811cef2154e7bc0f6c359c3be33 to your computer and use it in GitHub Desktop.
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