Skip to content

Instantly share code, notes, and snippets.

@temoncher
Last active March 5, 2022 07:23
Show Gist options
  • Save temoncher/2f86776c0147f49a0790b98da5d52b42 to your computer and use it in GitHub Desktop.
Save temoncher/2f86776c0147f49a0790b98da5d52b42 to your computer and use it in GitHub Desktop.
Builder pattern for strongly typed Finite State Machines in TypeScript
type State = string;
type Message = string;
type StatesList = readonly State[];
type MessagesConfig = Record<State, Record<Message, State>>;
type OneOf<S extends StatesList> = S[number];
type Send<
SBConfig extends StateBuilderConfig<StatesList, State, State>,
MConfig extends MessagesConfig,
C extends keyof MConfig,
> = <M extends keyof MConfig[C]>(newState: M) => Machine<SBConfig, MConfig, MConfig[C][M]>;
type UselessSend<
SBConfig extends StateBuilderConfig<StatesList, State, State>,
MConfig extends MessagesConfig,
C extends keyof MConfig,
> = (newState: string) => Machine<SBConfig, MConfig, C>
type Machine<
SBConfig extends StateBuilderConfig<StatesList, State, State>,
MConfig extends MessagesConfig,
C extends keyof MConfig,
> = {
currentState: C,
send: C extends SBConfig['final'] ? UselessSend<SBConfig, MConfig, C> : Send<SBConfig, MConfig, C>,
}
namespace Machine {
export const create = <
SBConfig extends StateBuilderConfig<StatesList, State, State>,
MConfig extends MessagesConfig,
C extends OneOf<SBConfig['states']>,
>(state: C): Machine<SBConfig, MConfig, C> => ({
currentState: state,
send:(message) => undefined as any,
});
}
type StateMessagesBuilder<
SBConfig extends StateBuilderConfig<StatesList, State, State>,
C extends Exclude<OneOf<SBConfig['states']>, SBConfig['final'] | keyof MConfig>,
MConfig extends MessagesConfig = {},
> = {
on: <
M extends Message,
S extends Exclude<OneOf<SBConfig['states']>, C>,
>(message: M, state: S) => StateMessagesBuilder<
SBConfig,
C,
MConfig & {
[state in C]: state extends keyof MConfig
? MConfig[state] & { [msg in M]: S }
: { [msg in M]: S }
}
>,
done: () => MessagesBuilder<SBConfig, MConfig>,
}
type MessagesBuilder<
SBConfig extends StateBuilderConfig<StatesList, State, State>,
MConfig extends MessagesConfig = {},
> = Exclude<OneOf<SBConfig['states']>, SBConfig['final']> extends keyof MConfig
? {
build: () => Machine<SBConfig, MConfig, SBConfig['initial']>,
} : {
when: <C extends Exclude<OneOf<SBConfig['states']>, SBConfig['final'] | keyof MConfig>>(
state: C
) => StateMessagesBuilder<SBConfig, C, MConfig>,
mc: MConfig,
};
type StateBuilderConfig<
S extends StatesList,
I extends OneOf<S>,
F extends OneOf<S>,
> = {
states: S,
initial: I,
final: F,
}
type StateBuilder = {
states: <S extends StatesList>(states:S) => ({
initial: <I extends OneOf<S>>(initialState: I) => ({
final: <F extends Exclude<OneOf<S>, I>>(finalState: F) => MessagesBuilder<StateBuilderConfig<S, I, F>>
})
})
}
const machine = (): StateBuilder => undefined as any;
const someMachine = machine()
.states(['idle', 'loading', 'active', 'finished'] as const)
.initial('idle')
.final('finished')
.when('idle')
.on('GO_TO_ACTIVE', 'active')
.done()
.when('active')
.on('GO_TO_IDLE', 'idle')
.on('GO_TO_LOADING', 'loading')
.done()
.when('loading')
.on('GO_BACK_TO_IDLE', 'idle')
.on('GO_TO_FINISHED', 'finished')
.done()
.build();
const finishState = someMachine.send('GO_TO_ACTIVE')
.send('GO_TO_IDLE')
.send('GO_TO_ACTIVE')
.send('GO_TO_LOADING')
.send('GO_BACK_TO_IDLE')
.send('GO_TO_ACTIVE')
.send('GO_TO_LOADING')
.send('GO_TO_FINISHED')
.currentState;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment