diff --git a/.changeset/modern-spies-cry.md b/.changeset/modern-spies-cry.md new file mode 100644 index 0000000000..15d3c67307 --- /dev/null +++ b/.changeset/modern-spies-cry.md @@ -0,0 +1,5 @@ +--- +'@xstate/fsm': patch +--- + +Pass around `TState['value']` type to `Transition` and `.initial` property of the machine configuration. diff --git a/packages/xstate-fsm/src/index.ts b/packages/xstate-fsm/src/index.ts index 37fdacc8a2..594c48a26d 100644 --- a/packages/xstate-fsm/src/index.ts +++ b/packages/xstate-fsm/src/index.ts @@ -130,7 +130,7 @@ function handleActions< export function createMachine< TContext extends object, TEvent extends EventObject = EventObject, - TState extends Typestate = { value: any; context: TContext } + TState extends Typestate = Typestate >( fsmConfig: StateMachine.Config, implementations: { @@ -182,7 +182,7 @@ export function createMachine< if (stateConfig.on) { const transitions: Array< - StateMachine.Transition + StateMachine.Transition > = toArray(stateConfig.on[eventObject.type]); for (const transition of transitions) { @@ -190,10 +190,13 @@ export function createMachine< return createUnchangedState(value, context); } - const { target, actions = [], cond = () => true } = - typeof transition === 'string' - ? { target: transition } - : transition; + const { + target, + actions = [], + cond = () => true + } = typeof transition === 'string' + ? { target: transition } + : transition; const isTargetless = target === undefined; @@ -209,11 +212,12 @@ export function createMachine< } if (cond(context, eventObject)) { - const allActions = (isTargetless - ? toArray(actions) - : ([] as any[]) - .concat(stateConfig.exit, actions, nextStateConfig.entry) - .filter((a) => a) + const allActions = ( + isTargetless + ? toArray(actions) + : ([] as any[]) + .concat(stateConfig.exit, actions, nextStateConfig.entry) + .filter((a) => a) ).map>((action) => toActionObject(action, (machine as any)._options.actions) ); diff --git a/packages/xstate-fsm/src/types.ts b/packages/xstate-fsm/src/types.ts index 6db57be054..8cca2eb137 100644 --- a/packages/xstate-fsm/src/types.ts +++ b/packages/xstate-fsm/src/types.ts @@ -86,10 +86,14 @@ export namespace StateMachine { assignment: Assigner | PropertyAssigner; } - export type Transition = - | string + export type Transition< + TContext extends object, + TEvent extends EventObject, + TStateValue extends string = string + > = + | TStateValue | { - target?: string; + target?: TStateValue; actions?: SingleOrArray>; cond?: (context: TContext, event: TEvent) => boolean; }; @@ -104,9 +108,15 @@ export namespace StateMachine { changed?: boolean | undefined; matches: ( value: TSV - ) => this is TState extends { value: TSV } - ? TState & { value: TSV } - : never; + ) => this is State< + (TState extends any + ? { value: TSV; context: any } extends TState + ? TState + : never + : never)['context'], + TEvent, + TState + > & { value: TSV }; } export type AnyMachine = StateMachine.Machine; @@ -118,16 +128,20 @@ export namespace StateMachine { export interface Config< TContext extends object, TEvent extends EventObject, - TState extends Typestate = { value: any; context: TContext } + TState extends Typestate = Typestate > { id?: string; - initial: string; + initial: TState['value']; context?: TContext; states: { [key in TState['value']]: { on?: { [K in TEvent['type']]?: SingleOrArray< - Transition + Transition< + TContext, + TEvent extends { type: K } ? TEvent : never, + TState['value'] + > >; }; exit?: SingleOrArray>; @@ -157,9 +171,7 @@ export namespace StateMachine { TState extends Typestate = { value: any; context: TContext } > { send: (event: TEvent | TEvent['type']) => void; - subscribe: ( - listener: StateListener> - ) => { + subscribe: (listener: StateListener>) => { unsubscribe: () => void; }; start: ( diff --git a/packages/xstate-fsm/test/types.test.ts b/packages/xstate-fsm/test/types.test.ts index 6072380251..4aa202c425 100644 --- a/packages/xstate-fsm/test/types.test.ts +++ b/packages/xstate-fsm/test/types.test.ts @@ -18,4 +18,44 @@ describe('matches', () => { ((_accept: string) => {})(state.context.count); } }); + + it('should narrow context using typestates', () => { + type MyContext = { user: { name: string } | null; count: number }; + type Typestates = + | { + value: 'idle'; + context: { user: null; count: number }; + } + | { + value: 'fetched'; + context: { user: { name: string }; count: number }; + }; + const machine = createMachine({ + context: { + user: null, + count: 0 + }, + initial: 'idle', + states: { idle: {}, fetched: {} } + }); + const state = machine.initialState; + + if (state.matches('idle')) { + ((_accept: null) => {})(state.context.user); + // @ts-expect-error + ((_accept: { name: string }) => {})(state.context.user); + + ((_accept: number) => {})(state.context.count); + // @ts-expect-error + ((_accept: string) => {})(state.context.count); + } else if (state.matches('fetched')) { + // @ts-expect-error + ((_accept: null) => {})(state.context.user); + ((_accept: { name: string }) => {})(state.context.user); + + ((_accept: number) => {})(state.context.count); + // @ts-expect-error + ((_accept: string) => {})(state.context.count); + } + }); });