diff --git a/packages/core/src/State.ts b/packages/core/src/State.ts index 42d2faab77..1f660bdc57 100644 --- a/packages/core/src/State.ts +++ b/packages/core/src/State.ts @@ -3,7 +3,7 @@ import { $$ACTOR_TYPE } from './interpreter.ts'; import type { StateNode } from './StateNode.ts'; import type { StateMachine } from './StateMachine.ts'; import { getStateValue } from './stateUtils.ts'; -import { TypegenDisabled, TypegenEnabled } from './typegenTypes.ts'; +import { TypegenDisabled } from './typegenTypes.ts'; import type { ProvidedActor, AnyMachineSnapshot, @@ -11,16 +11,28 @@ import type { EventObject, HistoryValue, MachineContext, - Prop, StateConfig, StateValue, AnyActorRef, - EventDescriptor, Snapshot, - ParameterizedObject + ParameterizedObject, + IsNever } from './types.ts'; import { matchesState } from './utils.ts'; +type ToTestStateValue = + TStateValue extends string + ? TStateValue + : IsNever extends true + ? never + : + | keyof TStateValue + | { + [K in keyof TStateValue]?: ToTestStateValue< + NonNullable + >; + }; + export function isMachineSnapshot< TContext extends MachineContext, TEvent extends EventObject @@ -37,6 +49,7 @@ interface MachineSnapshotBase< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TOutput, TResolvedTypesMeta = TypegenDisabled @@ -49,13 +62,14 @@ interface MachineSnapshotBase< ParameterizedObject, ParameterizedObject, string, + TStateValue, TTag, unknown, TOutput, TResolvedTypesMeta >; tags: Set; - value: StateValue; + value: TStateValue; status: 'active' | 'done' | 'error' | 'stopped'; error: unknown; context: TContext; @@ -74,20 +88,17 @@ interface MachineSnapshotBase< * Whether the current state value is a subset of the given parent state value. * @param testValue */ - matches: < - TSV extends TResolvedTypesMeta extends TypegenEnabled - ? Prop, 'matchesStates'> - : StateValue - >( + matches: ( this: MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta >, - testValue: TSV + testValue: ToTestStateValue ) => boolean; /** @@ -99,6 +110,7 @@ interface MachineSnapshotBase< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -119,6 +131,7 @@ interface MachineSnapshotBase< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -131,6 +144,7 @@ interface MachineSnapshotBase< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -142,6 +156,7 @@ interface MachineSnapshotBase< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -153,6 +168,7 @@ interface ActiveMachineSnapshot< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TOutput, TResolvedTypesMeta = TypegenDisabled @@ -160,6 +176,7 @@ interface ActiveMachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -173,6 +190,7 @@ interface DoneMachineSnapshot< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TOutput, TResolvedTypesMeta = TypegenDisabled @@ -180,6 +198,7 @@ interface DoneMachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -193,6 +212,7 @@ interface ErrorMachineSnapshot< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TOutput, TResolvedTypesMeta = TypegenDisabled @@ -200,6 +220,7 @@ interface ErrorMachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -213,6 +234,7 @@ interface StoppedMachineSnapshot< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TOutput, TResolvedTypesMeta = TypegenDisabled @@ -220,6 +242,7 @@ interface StoppedMachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -233,6 +256,7 @@ export type MachineSnapshot< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TOutput, TResolvedTypesMeta = TypegenDisabled @@ -241,6 +265,7 @@ export type MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -249,6 +274,7 @@ export type MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -257,6 +283,7 @@ export type MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -265,6 +292,7 @@ export type MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -334,6 +362,7 @@ export function createMachineSnapshot< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TResolvedTypesMeta = TypegenDisabled >( @@ -343,6 +372,7 @@ export function createMachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, undefined, TResolvedTypesMeta @@ -354,12 +384,11 @@ export function createMachineSnapshot< machine, context: config.context, _nodes: config._nodes, - value: getStateValue(machine.root, config._nodes), + value: getStateValue(machine.root, config._nodes) as never, tags: new Set(config._nodes.flatMap((sn) => sn.tags)), children: config.children as any, historyValue: config.historyValue || {}, - // this one is generic in the target and it's hard to create a matching non-generic source signature - matches: machineSnapshotMatches as any, + matches: machineSnapshotMatches as never, hasTag: machineSnapshotHasTag, can: machineSnapshotCan, getMeta: machineSnapshotGetMeta, @@ -381,6 +410,7 @@ export function getPersistedState< TContext extends MachineContext, TEvent extends EventObject, TChildren extends Record, + TStateValue extends StateValue, TTag extends string, TOutput, TResolvedTypesMeta = TypegenDisabled @@ -389,6 +419,7 @@ export function getPersistedState< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 87d05259de..46ec53d58d 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -50,7 +50,8 @@ import type { SnapshotFrom, Snapshot, AnyActorLogic, - HistoryValue + HistoryValue, + StateSchema } from './types.ts'; import { getAllOwnEventDescriptors, @@ -71,6 +72,7 @@ export class StateMachine< TAction extends ParameterizedObject, TGuard extends ParameterizedObject, TDelay extends string, + TStateValue extends StateValue, TTag extends string, TInput, TOutput, @@ -89,6 +91,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -105,19 +108,6 @@ export class StateMachine< public implementations: MachineImplementationsSimplified; - public types: MachineTypes< - TContext, - TEvent, - TActor, - TAction, - TGuard, - TDelay, - TTag, - TInput, - TOutput, - TResolvedTypesMeta - >; - public __xstatenode: true = true; public idMap: Map> = new Map(); @@ -155,7 +145,6 @@ export class StateMachine< guards: implementations?.guards ?? {} }; this.version = this.config.version; - this.types = this.config.types ?? ({} as any as this['types']); this.transition = this.transition.bind(this); this.getInitialState = this.getInitialState.bind(this); @@ -208,6 +197,7 @@ export class StateMachine< TAction, TGuard, TDelay, + TStateValue, TTag, TInput, TOutput, @@ -240,6 +230,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -266,6 +257,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -273,27 +265,29 @@ export class StateMachine< } /** - * Determines the next state given the current `state` and received `event`. + * Determines the next snapshot given the current `snapshot` and received `event`. * Calculates a full macrostep from all microsteps. * - * @param state The current State instance or state value + * @param snapshot The current snapshot * @param event The received event */ public transition( - state: MachineSnapshot< + snapshot: MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta >, event: TEvent, - actorScope: ActorScope + actorScope: ActorScope ): MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -301,19 +295,19 @@ export class StateMachine< // TODO: handle error events in a better way if ( isErrorActorEvent(event) && - !getAllOwnEventDescriptors(state).some( + !getAllOwnEventDescriptors(snapshot).some( (nextEvent) => nextEvent === event.type ) ) { - return cloneMachineSnapshot(state, { + return cloneMachineSnapshot(snapshot, { status: 'error', error: event.data }); } - const { state: nextState } = macrostep(state, event, actorScope); + const { state: nextState } = macrostep(snapshot, event, actorScope); - return nextState as typeof state; + return nextState as typeof snapshot; } /** @@ -328,6 +322,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -339,6 +334,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -348,17 +344,18 @@ export class StateMachine< } public getTransitionData( - state: MachineSnapshot< + snapshot: MachineSnapshot< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta >, event: TEvent ): Array> { - return transitionNode(this.root, state.value, state, event) || []; + return transitionNode(this.root, snapshot.value, snapshot, event) || []; } /** @@ -373,6 +370,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -414,6 +412,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -425,6 +424,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -469,6 +469,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -512,6 +513,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -528,6 +530,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -538,6 +541,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -591,6 +595,7 @@ export class StateMachine< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta diff --git a/packages/core/src/createMachine.ts b/packages/core/src/createMachine.ts index 89d4b4f3cd..52e44d1b00 100644 --- a/packages/core/src/createMachine.ts +++ b/packages/core/src/createMachine.ts @@ -9,15 +9,61 @@ import { AnyEventObject, Cast, InternalMachineImplementations, + IsNever, MachineConfig, MachineContext, NonReducibleUnknown, ParameterizedObject, Prop, ProvidedActor, + StateValue, ToChildren } from './types.ts'; +type TestValue = + | string + | { + [k: string]: TestValue | undefined; + }; + +type _GroupTestValues = + TTestValue extends string + ? TTestValue extends `${string}.${string}` + ? [never, never] + : [TTestValue, never] + : [never, TTestValue]; +type GroupTestValues = { + leafCandidates: _GroupTestValues[0]; + nonLeaf: _GroupTestValues[1]; +}; + +type FilterLeafValues< + TLeafCandidate extends string, + TNonLeaf extends { [k: string]: TestValue | undefined } +> = IsNever extends true + ? TLeafCandidate + : TLeafCandidate extends string + ? TLeafCandidate extends keyof TNonLeaf + ? never + : TLeafCandidate + : never; + +// this is not 100% accurate since we can't make parallel regions required in the result +// `TTestValue` doesn't encode this information anyhow for us to be able to do that +// this is fine for most practical use cases anyway though +type ToStateValue = + | FilterLeafValues< + GroupTestValues['leafCandidates'], + GroupTestValues['nonLeaf'] + > + | (IsNever['nonLeaf']> extends false + ? { + [K in keyof GroupTestValues['nonLeaf']]: ToStateValue< + NonNullable['nonLeaf'][K]> + >; + } + : never); + export function createMachine< TContext extends MachineContext, TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here @@ -65,6 +111,9 @@ export function createMachine< TAction, TGuard, TDelay, + 'matchesStates' extends keyof TTypesMeta + ? ToStateValue> + : StateValue, Prop< ResolveTypegenMeta< TTypesMeta, @@ -93,6 +142,7 @@ export function createMachine< any, any, any, + any, any >(config as any, implementations as any); } diff --git a/packages/core/src/setup.ts b/packages/core/src/setup.ts index cfeab03f14..c5cd2a6995 100644 --- a/packages/core/src/setup.ts +++ b/packages/core/src/setup.ts @@ -8,6 +8,7 @@ import { AnyActorRef, AnyEventObject, Cast, + ConditionalRequired, DelayConfig, Invert, IsNever, @@ -16,6 +17,7 @@ import { NonReducibleUnknown, ParameterizedObject, SetupTypes, + StateSchema, ToChildren, Values } from './types'; @@ -60,6 +62,43 @@ type ToProvidedActor< }; }>; +type _GroupStateKeys< + T extends StateSchema, + S extends keyof T['states'] +> = S extends any + ? T['states'][S] extends { type: 'history' } + ? [never, never] + : T extends { type: 'parallel' } + ? [S, never] + : 'states' extends keyof T['states'][S] + ? [S, never] + : [never, S] + : never; + +type GroupStateKeys = { + nonLeaf: _GroupStateKeys[0]; + leaf: _GroupStateKeys[1]; +}; + +type ToStateValue = T extends { + states: Record; +} + ? IsNever extends true + ? {} + : + | GroupStateKeys['leaf'] + | (IsNever['nonLeaf']> extends false + ? ConditionalRequired< + { + [K in GroupStateKeys['nonLeaf']]?: ToStateValue< + T['states'][K] + >; + }, + T extends { type: 'parallel' } ? true : false + > + : never) + : {}; + export function setup< TContext extends MachineContext, TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here @@ -142,6 +181,7 @@ export function setup< ToParameterizedObject, ToParameterizedObject, TDelay, + ToStateValue, TTag, TInput, TOutput, diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 9ba6a82e00..52de1ca0ca 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -750,7 +750,7 @@ export function transitionNode< >( stateNode: AnyStateNode, stateValue: StateValue, - state: MachineSnapshot, + state: MachineSnapshot, event: TEvent ): Array> | undefined { // leaf node diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index af5dacd63b..5b541161fd 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -112,6 +112,7 @@ export interface UnifiedArg< TContext, TEvent, Record, // TODO: this should be replaced with `TChildren` + StateValue, string, unknown > @@ -135,6 +136,7 @@ export type InputFrom = T extends StateMachine< infer _TAction, infer _TGuard, infer _TDelay, + infer _TStateValue, infer _TTag, infer TInput, infer _TOutput, @@ -932,7 +934,15 @@ export type AnyStateNode = StateNode; export type AnyStateNodeDefinition = StateNodeDefinition; -export type AnyMachineSnapshot = MachineSnapshot; +export type AnyMachineSnapshot = MachineSnapshot< + any, + any, + any, + any, + any, + any, + any +>; /** @deprecated use `AnyMachineSnapshot` instead */ export type AnyState = AnyMachineSnapshot; @@ -945,6 +955,7 @@ export type AnyStateMachine = StateMachine< any, // action any, // guard any, // delay + any, // state value any, // tag any, // input any, // output @@ -1593,6 +1604,7 @@ export type Mapper< TContext, TEvent, Record, // TODO: this should be replaced with `TChildren` + StateValue, string, unknown > @@ -1951,6 +1963,7 @@ export type ActorRefFrom = ReturnTypeOrValue extends infer R infer _TAction, infer _TGuard, infer _TDelay, + infer TStateValue, infer TTag, infer _TInput, infer TOutput, @@ -1962,6 +1975,7 @@ export type ActorRefFrom = ReturnTypeOrValue extends infer R TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, AreAllImplementationsAssumedToBeProvided extends false @@ -1996,6 +2010,7 @@ export type InterpreterFrom< infer _TAction, infer _TGuard, infer _TDelay, + infer TStateValue, infer TTag, infer TInput, infer TOutput, @@ -2007,6 +2022,7 @@ export type InterpreterFrom< TContext, TEvent, TChildren, + TStateValue, TTag, TOutput, TResolvedTypesMeta @@ -2029,6 +2045,7 @@ export type MachineImplementationsFrom< infer _TAction, infer _TGuard, infer _TDelay, + infer _TStateValue, infer _TTag, infer _TInput, infer _TOutput, @@ -2173,25 +2190,11 @@ export type SnapshotFrom = ReturnTypeOrValue extends infer R ? TSnapshot : R extends Actor ? SnapshotFrom - : R extends StateMachine< - infer _TContext, - infer _TEvent, - infer _TChildren, - infer _TActor, - infer _TAction, - infer _TGuard, - infer _TDelay, - infer _TTag, - infer _TInput, - infer _TOutput, - infer _TResolvedTypesMeta - > - ? StateFrom - : R extends ActorLogic - ? ReturnType - : R extends ActorScope - ? TSnapshot - : never + : R extends ActorLogic + ? ReturnType + : R extends ActorScope + ? TSnapshot + : never : never; export type EventFromLogic> = @@ -2208,6 +2211,7 @@ type ResolveEventType = ReturnTypeOrValue extends infer R infer _TAction, infer _TGuard, infer _TDelay, + infer _TStateValue, infer _TTag, infer _TInput, infer _TOutput, @@ -2243,6 +2247,7 @@ export type ContextFrom = ReturnTypeOrValue extends infer R infer _TAction, infer _TGuard, infer _TDelay, + infer _TStateValue, infer _TTag, infer _TInput, infer _TOutput, @@ -2368,3 +2373,7 @@ export type ToChildren = ? 'include' : 'exclude'] >; + +export type StateSchema = { + states?: Record; +}; diff --git a/packages/core/test/setup.types.test.ts b/packages/core/test/setup.types.test.ts index a203a49cfc..887b4b3931 100644 --- a/packages/core/test/setup.types.test.ts +++ b/packages/core/test/setup.types.test.ts @@ -924,6 +924,184 @@ describe('setup()', () => { | undefined; }); + it('should type the snapshot state value of a stateless machine as an empty object', () => { + const machine = setup({}).createMachine({}); + + const snapshot = createActor(machine).getSnapshot(); + + type ExpectedType = {}; + + snapshot.value satisfies ExpectedType; + ({}) as ExpectedType satisfies ExpectedType; + + // @ts-expect-error + snapshot.value.unknown; + }); + + it('should type the snapshot state value of a simple FSM as a union of strings', () => { + const machine = setup({}).createMachine({ + initial: 'a', + states: { + a: {}, + b: {} + } + }); + + const snapshot = createActor(machine).getSnapshot(); + + type ExpectedType = 'a' | 'b'; + + snapshot.value satisfies ExpectedType; + ({}) as ExpectedType satisfies ExpectedType; + }); + + it('should type the snapshot state value without including history state keys', () => { + const machine = setup({}).createMachine({ + initial: 'a', + states: { + a: {}, + b: {}, + c: { + type: 'history' + } + } + }); + + const snapshot = createActor(machine).getSnapshot(); + + type ExpectedType = 'a' | 'b'; + + snapshot.value satisfies ExpectedType; + ({}) as ExpectedType satisfies ExpectedType; + }); + + it('should type the snapshot state value of a nested statechart using optional properties for parent states keys', () => { + const machine = setup({}).createMachine({ + initial: 'a', + states: { + a: { + initial: 'a1', + states: { + a1: {}, + a2: {} + } + }, + b: { + initial: 'b1', + states: { + b1: {}, + b2: {} + } + } + } + }); + + const snapshot = createActor(machine).getSnapshot(); + + type ExpectedType = { + a?: 'a1' | 'a2'; + b?: 'b1' | 'b2'; + }; + + snapshot.value satisfies ExpectedType; + ({}) as ExpectedType satisfies typeof snapshot.value; + }); + + it('should type the snapshot state value of a parallel state using required properties for its children', () => { + const machine = setup({}).createMachine({ + type: 'parallel', + states: { + a: { + initial: 'a1', + states: { + a1: {}, + a2: {} + } + }, + b: { + initial: 'b1', + states: { + b1: {}, + b2: {} + } + } + } + }); + + const snapshot = createActor(machine).getSnapshot(); + + type ExpectedType = { + a: 'a1' | 'a2'; + b: 'b1' | 'b2'; + }; + + snapshot.value satisfies ExpectedType; + ({}) as ExpectedType satisfies typeof snapshot.value; + }); + + it('should type the snapshot state value of an empty parallel region as an empty object', () => { + const machine = setup({}).createMachine({ + type: 'parallel', + states: { + a: {}, + b: { + initial: 'b1', + states: { + b1: {}, + b2: {} + } + } + } + }); + + const snapshot = createActor(machine).getSnapshot(); + + type ExpectedType = { + a: {}; + b: 'b1' | 'b2'; + }; + + snapshot.value satisfies ExpectedType; + ({}) as ExpectedType satisfies typeof snapshot.value; + }); + + it('should type the snapshot state value of a statechart with nested compound states', () => { + const machine = setup({}).createMachine({ + initial: 'a', + states: { + a: {}, + b: { + initial: 'b1', + states: { + b1: { + initial: 'b11', + states: { + b11: {}, + b12: {} + } + }, + b2: {} + } + } + } + }); + + const snapshot = createActor(machine).getSnapshot(); + + type ExpectedType = + | 'a' + | { + b?: + | 'b2' + | { + b1?: 'b11' | 'b12'; + }; + }; + + snapshot.value satisfies ExpectedType; + ({}) as ExpectedType satisfies typeof snapshot.value; + }); + it('should accept `assign` when no actor and children types are provided', () => { setup({}).createMachine({ on: { @@ -933,4 +1111,245 @@ describe('setup()', () => { } }); }); + + it('should not allow matching against any value when the machine has no states', () => { + const machine = setup({}).createMachine({}); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches( + // @ts-expect-error + {} + ); + snapshot.matches( + // @ts-expect-error + 'pending' + ); + snapshot.matches( + // @ts-expect-error + { + foo: 'pending' + } + ); + }); + + it('should allow matching against a valid string value of a simple FSM', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: {}, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches('green'); + snapshot.matches('yellow'); + snapshot.matches('red'); + }); + + it('should not allow matching against a invalid string value of a simple FSM', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: {}, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches( + // @ts-expect-error + 'orange' + ); + }); + + it('should not allow matching against an empty object value of a simple FSM', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: {}, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches( + // @ts-expect-error + {} + ); + }); + + it('should not allow matching against an object value with a key that is a valid value of a simple FSM', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: {}, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches( + // @ts-expect-error + { + green: {} + } + ); + }); + + it('should allow matching against valid top state keys of a statechart with nested compound states', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: { + initial: 'walk', + states: { + walk: {}, + wait: {} + } + }, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches('green'); + snapshot.matches('yellow'); + snapshot.matches('red'); + }); + + it('should not allow matching against an invalid top state key of a statechart with nested compound states', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: { + initial: 'walk', + states: { + walk: {}, + wait: {} + } + }, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches( + // @ts-expect-error + 'orange' + ); + }); + + it('should allow matching against a valid full object value of a statechart with nested compound states', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: { + initial: 'walk', + states: { + walk: {}, + wait: {} + } + }, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches({ + green: 'wait' + }); + }); + + it('should allow matching against a valid non-full object value of a statechart with nested compound states', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: { + initial: 'walk', + states: { + walk: { + initial: 'steady', + states: { + steady: {}, + slowingDown: {} + } + }, + wait: {} + } + }, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches({ + green: 'wait' + }); + }); + + it('should not allow matching against a invalid object value of a statechart with nested compound states', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: { + initial: 'walk', + states: { + walk: {}, + wait: {} + } + }, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches({ + // @ts-expect-error + green: 'invalid' + }); + }); + + it('should not allow matching against a invalid object value with self-key at value position', () => { + const machine = setup({}).createMachine({ + initial: 'green', + states: { + green: { + initial: 'walk', + states: { + walk: {}, + wait: {} + } + }, + yellow: {}, + red: {} + } + }); + + const snapshot = createActor(machine).start().getSnapshot(); + + snapshot.matches({ + // @ts-expect-error + green: 'green' + }); + }); }); diff --git a/packages/core/test/typegenTypes.test.ts b/packages/core/test/typegenTypes.test.ts index 26c1808923..7993e6584b 100644 --- a/packages/core/test/typegenTypes.test.ts +++ b/packages/core/test/typegenTypes.test.ts @@ -304,9 +304,9 @@ describe('typegen types', () => { matchesStates: 'a' | 'b' | 'c'; missingImplementations: { actions: never; + actors: never; delays: never; guards: never; - actors: never; }; } @@ -333,9 +333,9 @@ describe('typegen types', () => { matchesStates: 'a' | { a: 'b' } | { a: 'c' }; missingImplementations: { actions: never; + actors: never; delays: never; guards: never; - actors: never; }; } @@ -1066,7 +1066,7 @@ describe('typegen types', () => { it("shouldn't end up with `never` within a branch after two `state.matches` calls", () => { interface TypesMeta extends TypegenMeta { - matchesStates: 'a' | 'a.b'; + matchesStates: 'a' | 'a.b' | { a?: 'b' }; missingImplementations: { actions: never; delays: never; @@ -1089,7 +1089,7 @@ describe('typegen types', () => { const state = createActor(machine).getSnapshot(); - if (state.matches('a') && state.matches('a.b')) { + if (state.matches('a') && state.matches({ a: 'b' })) { ((_accept: string) => {})(state.context.foo); } }); diff --git a/packages/xstate-react/src/createActorContext.ts b/packages/xstate-react/src/createActorContext.ts index b8fb9014df..ad1a5b73c3 100644 --- a/packages/xstate-react/src/createActorContext.ts +++ b/packages/xstate-react/src/createActorContext.ts @@ -21,6 +21,7 @@ type ToMachinesWithProvidedImplementations = infer TAction, infer TGuard, infer TDelay, + infer TStateValue, infer TTag, infer TInput, infer TOutput, @@ -34,6 +35,7 @@ type ToMachinesWithProvidedImplementations = TAction, TGuard, TDelay, + TStateValue, TTag, TInput, TOutput, diff --git a/packages/xstate-test/src/machine.ts b/packages/xstate-test/src/machine.ts index 83f68c79bf..de932ade85 100644 --- a/packages/xstate-test/src/machine.ts +++ b/packages/xstate-test/src/machine.ts @@ -75,6 +75,7 @@ function serializeMachineTransition( MachineContext, EventObject, Record, + StateValue, string, unknown >, @@ -84,6 +85,7 @@ function serializeMachineTransition( MachineContext, EventObject, Record, + StateValue, string, unknown >