From 932680fc6d6700b9b713cb32d9037ccc37f21bff Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 12 Dec 2021 07:20:08 -0700 Subject: [PATCH 1/4] Remove Typestate --- packages/core/actions/ExecutableAction.ts | 2 +- packages/core/src/Machine.ts | 16 +-- packages/core/src/State.ts | 35 ++--- packages/core/src/StateMachine.ts | 30 ++-- packages/core/src/actor.ts | 7 +- packages/core/src/behaviors.ts | 2 +- packages/core/src/interpreter.ts | 79 ++++------- packages/core/src/invoke.ts | 2 +- packages/core/src/model.types.ts | 3 +- packages/core/src/stateUtils.ts | 16 +-- packages/core/src/types.ts | 64 ++++----- packages/core/src/utils.ts | 4 +- packages/core/test/interpreter.test.ts | 4 +- packages/core/test/match.test.ts | 132 +----------------- packages/core/test/transient.test.ts | 4 +- packages/core/test/types.test.ts | 80 +---------- packages/xstate-graph/src/graph.ts | 2 +- packages/xstate-inspect/src/inspectMachine.ts | 2 +- packages/xstate-inspect/src/serialize.ts | 2 +- packages/xstate-inspect/src/types.ts | 2 +- packages/xstate-react/src/useInterpret.ts | 12 +- packages/xstate-react/src/useMachine.ts | 42 +++--- .../xstate-react/src/useReactEffectActions.ts | 8 +- packages/xstate-react/src/useSelector.ts | 2 +- packages/xstate-react/src/useService.ts | 17 +-- packages/xstate-react/test/types.test.tsx | 28 +--- .../xstate-react/test/useMachine.test.tsx | 28 ++-- packages/xstate-scxml/src/index.ts | 2 +- packages/xstate-svelte/src/useMachine.ts | 11 +- packages/xstate-test/src/index.ts | 4 +- packages/xstate-vue/src/useInterpret.ts | 12 +- packages/xstate-vue/src/useMachine.ts | 17 +-- packages/xstate-vue/src/useService.ts | 10 +- 33 files changed, 183 insertions(+), 498 deletions(-) diff --git a/packages/core/actions/ExecutableAction.ts b/packages/core/actions/ExecutableAction.ts index a283155d72..3bdff3cf54 100644 --- a/packages/core/actions/ExecutableAction.ts +++ b/packages/core/actions/ExecutableAction.ts @@ -34,7 +34,7 @@ export class ExecutableAction< this.type = actionObject.type; this.params = actionObject.params ?? {}; } - public execute(state: State) { + public execute(state: State) { const context = this.context ?? state.context; return this._exec?.(context, state.event, { diff --git a/packages/core/src/Machine.ts b/packages/core/src/Machine.ts index f31ad27a61..11e4e84c05 100644 --- a/packages/core/src/Machine.ts +++ b/packages/core/src/Machine.ts @@ -3,29 +3,23 @@ import { MachineConfig, EventObject, AnyEventObject, - Typestate, MachineContext } from './types'; import { StateMachine } from './StateMachine'; export function createMachine< TContext extends MachineContext, - TEvent extends EventObject = AnyEventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject = AnyEventObject >( config: MachineConfig, options?: Partial> -): StateMachine; +): StateMachine; export function createMachine< TContext extends MachineContext, - TEvent extends EventObject = AnyEventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject = AnyEventObject >( definition: MachineConfig, implementations?: Partial> -): StateMachine { - return new StateMachine( - definition, - implementations - ); +): StateMachine { + return new StateMachine(definition, implementations); } diff --git a/packages/core/src/State.ts b/packages/core/src/State.ts index 965b172118..95b570481d 100644 --- a/packages/core/src/State.ts +++ b/packages/core/src/State.ts @@ -5,7 +5,6 @@ import type { StateConfig, SCXML, TransitionDefinition, - Typestate, HistoryValue, ActorRef, MachineContext, @@ -26,9 +25,8 @@ import type { StateMachine } from './StateMachine'; export function isState< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } ->(state: object | string): state is State { + TEvent extends EventObject +>(state: object | string): state is State { if (isString(state)) { return false; } @@ -38,12 +36,11 @@ export function isState< export class State< TContext extends MachineContext, - TEvent extends EventObject = EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject = EventObject > { public value: StateValue; public context: TContext; - public history?: State; + public history?: State; public historyValue: HistoryValue = {}; public actions: BaseActionObject[] = []; public meta: any = {}; @@ -79,7 +76,7 @@ export class State< */ public children: Record>; public tags: Set; - public machine: StateMachine | undefined; + public machine: StateMachine | undefined; /** * Creates a new State instance for the given `stateValue` and `context`. * @param stateValue @@ -89,9 +86,9 @@ export class State< TContext extends MachineContext, TEvent extends EventObject = EventObject >( - stateValue: State | StateValue, + stateValue: State | StateValue, context: TContext = {} as TContext - ): State { + ): State { if (stateValue instanceof State) { if (stateValue.context !== context) { return new State({ @@ -133,7 +130,7 @@ export class State< public static create< TContext extends MachineContext, TEvent extends EventObject = EventObject - >(config: StateConfig): State { + >(config: StateConfig): State { return new State(config); } /** @@ -141,9 +138,7 @@ export class State< * @param stateValue * @param context */ - public static inert>( - state: TState - ): TState; + public static inert>(state: TState): TState; public static inert< TContext extends MachineContext, TEvent extends EventObject = EventObject @@ -241,17 +236,9 @@ export class State< * Whether the current state value is a subset of the given parent state value. * @param parentStateValue */ - public matches( + public matches( parentStateValue: TSV - ): this is State< - (TTypestate extends any - ? { value: TSV; context: any } extends TTypestate - ? TTypestate - : never - : never)['context'], - TEvent, - TTypestate - > & { value: TSV } { + ): this is State & { value: TSV } { return matchesState(parentStateValue as StateValue, this.value); } diff --git a/packages/core/src/StateMachine.ts b/packages/core/src/StateMachine.ts index 36e0462b30..a25dff52b4 100644 --- a/packages/core/src/StateMachine.ts +++ b/packages/core/src/StateMachine.ts @@ -6,7 +6,6 @@ import type { EventObject, MachineConfig, SCXML, - Typestate, Transitions, MachineSchema, StateNodeDefinition, @@ -59,8 +58,7 @@ function resolveContext( export class StateMachine< TContext extends MachineContext = any, - TEvent extends EventObject = EventObject, - TTypestate extends Typestate = any + TEvent extends EventObject = EventObject > { private _context: () => TContext; public get context(): TContext { @@ -172,7 +170,7 @@ export class StateMachine< * * @param state The state to resolve */ - public resolveState(state: State): typeof state { + public resolveState(state: State): typeof state { const configuration = Array.from( getConfiguration(getStateNodes(this.root, state.value)) ); @@ -191,9 +189,9 @@ export class StateMachine< * @param event The received event */ public transition( - state: StateValue | State = this.initialState, + state: StateValue | State = this.initialState, event: Event | SCXML.Event - ): State { + ): State { const currentState = toState(state, this); return macrostep(currentState, event, this); @@ -207,9 +205,9 @@ export class StateMachine< * @param event The received event */ public microstep( - state: StateValue | State = this.initialState, + state: StateValue | State = this.initialState, event: Event | SCXML.Event - ): State { + ): State { const resolvedState = toState(state, this); const _event = toSCXMLEvent(event); @@ -240,7 +238,7 @@ export class StateMachine< return transitionNode(this.root, state.value, state, _event) || []; } - public get first(): State { + public get first(): State { const pseudoinitial = this.resolveState( State.from( getStateValue(this.root, getConfiguration([this.root])), @@ -256,7 +254,7 @@ export class StateMachine< * The initial State instance, which includes all actions to be executed from * entering the initial state. */ - public get initialState(): State { + public get initialState(): State { const nextState = resolveMicroTransition(this, [], this.first, undefined); return macrostep(nextState, null as any, this); } @@ -264,13 +262,9 @@ export class StateMachine< /** * Returns the initial `State` instance, with reference to `self` as an `ActorRef`. */ - public getInitialState(): State { + public getInitialState(): State { const nextState = resolveMicroTransition(this, [], this.first, undefined); - return macrostep(nextState, null as any, this) as State< - TContext, - TEvent, - TTypestate - >; + return macrostep(nextState, null as any, this) as State; } public getStateNodeById(stateId: string): StateNode { @@ -297,11 +291,11 @@ export class StateMachine< public createState( stateConfig: State | StateConfig - ): State { + ): State { const state = stateConfig instanceof State ? stateConfig - : (new State(stateConfig) as State); + : (new State(stateConfig) as State); state.machine = this; return state; } diff --git a/packages/core/src/actor.ts b/packages/core/src/actor.ts index 9a08c5f5af..48fabaa5f6 100644 --- a/packages/core/src/actor.ts +++ b/packages/core/src/actor.ts @@ -104,10 +104,7 @@ export function spawnObservable( return spawn(createObservableBehavior(lazyObservable), name); } -export function spawnMachine( - machine: StateMachine, - name?: string -) { +export function spawnMachine(machine: StateMachine, name?: string) { return spawn(createMachineBehavior(machine), name); } @@ -130,7 +127,7 @@ export function spawnFrom< TEvent extends EventObject, TEmitted extends State >( - entity: StateMachine, + entity: StateMachine, name?: string ): ObservableActorRef; export function spawnFrom( diff --git a/packages/core/src/behaviors.ts b/packages/core/src/behaviors.ts index e95dbe6aee..da6b754ac6 100644 --- a/packages/core/src/behaviors.ts +++ b/packages/core/src/behaviors.ts @@ -492,7 +492,7 @@ export function createBehaviorFrom< TEvent extends EventObject, TEmitted extends State >( - entity: StateMachine + entity: StateMachine ): Behavior; export function createBehaviorFrom( entity: InvokeCallback diff --git a/packages/core/src/interpreter.ts b/packages/core/src/interpreter.ts index e3bda32b6c..6a8a6a2b48 100644 --- a/packages/core/src/interpreter.ts +++ b/packages/core/src/interpreter.ts @@ -11,7 +11,6 @@ import { ActionFunctionMap, SCXML, Observer, - Typestate, InvokeActionObject, AnyEventObject, ActorRef, @@ -53,9 +52,8 @@ import { isExecutableAction } from '../actions/ExecutableAction'; export type StateListener< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } -> = (state: State, event: TEvent) => void; + TEvent extends EventObject +> = (state: State, event: TEvent) => void; export type EventListener = ( event: TEvent @@ -97,14 +95,13 @@ declare global { export class Interpreter< TContext extends MachineContext, - TEvent extends EventObject = EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject = EventObject > { /** * The current state of the interpreted machine. */ - private _state?: State; - private _initialState?: State; + private _state?: State; + private _initialState?: State; /** * The clock that is responsible for setting and clearing timeouts, such as delayed events and transitions. */ @@ -114,9 +111,7 @@ export class Interpreter< public id: string; private scheduler: Scheduler = new Scheduler(); private delayedEventsMap: Record = {}; - private listeners: Set< - StateListener - > = new Set(); + private listeners: Set> = new Set(); private stopListeners: Set = new Set(); private errorListeners: Set = new Set(); private doneListeners: Set = new Set(); @@ -144,7 +139,7 @@ export class Interpreter< * @param options Interpreter options */ constructor( - public machine: StateMachine, + public machine: StateMachine, options?: Partial ) { const resolvedOptions: InterpreterOptions = { @@ -176,7 +171,7 @@ export class Interpreter< return this.status === InterpreterStatus.Running; } - public get initialState(): State { + public get initialState(): State { try { CapturedState.current = { actorRef: this.ref, @@ -198,7 +193,7 @@ export class Interpreter< } } - public get state(): State { + public get state(): State { return this._state!; } @@ -207,14 +202,14 @@ export class Interpreter< * * @param state The state whose actions will be executed */ - public execute(state: State): void { + public execute(state: State): void { for (const action of state.actions) { this.exec(action, state); } } private update( - state: State, + state: State, _event: SCXML.Event ): void { // Attach session ID to state @@ -257,9 +252,7 @@ export class Interpreter< * * @param listener The state listener */ - public onTransition( - listener: StateListener - ): this { + public onTransition(listener: StateListener): this { this.listeners.add(listener); // Send current state to listener @@ -271,17 +264,15 @@ export class Interpreter< } public subscribe( - nextListener?: (state: State) => void, + nextListener?: (state: State) => void, errorListener?: (error: any) => void, completeListener?: () => void ): Subscription; - public subscribe( - observer: Observer> - ): Subscription; + public subscribe(observer: Observer>): Subscription; public subscribe( nextListenerOrObserver?: - | ((state: State) => void) - | Observer>, + | ((state: State) => void) + | Observer>, errorListener?: (error: Error) => void, completeListener?: () => void ): Subscription { @@ -289,7 +280,7 @@ export class Interpreter< return { unsubscribe: () => void 0 }; } - let listener: (state: State) => void; + let listener: (state: State) => void; let resolvedCompleteListener = completeListener; if (typeof nextListenerOrObserver === 'function') { @@ -365,7 +356,7 @@ export class Interpreter< */ public onDone( listener: EventListener - ): Interpreter { + ): Interpreter { this.doneListeners.add(listener); return this; } @@ -376,7 +367,7 @@ export class Interpreter< */ public off( listener: (...args: any[]) => void - ): Interpreter { + ): Interpreter { this.listeners.delete(listener); this.stopListeners.delete(listener); this.doneListeners.delete(listener); @@ -389,8 +380,8 @@ export class Interpreter< * @param initialState The state to start the statechart from */ public start( - initialState?: State | StateValue - ): Interpreter { + initialState?: State | StateValue + ): Interpreter { if (this.status === InterpreterStatus.Running) { // Do not restart the service if it is already started return this; @@ -402,7 +393,7 @@ export class Interpreter< const resolvedState = initialState === undefined ? this.initialState - : isState(initialState) + : isState(initialState) ? this.machine.resolveState(initialState) : this.machine.resolveState( State.from(initialState, this.machine.context) @@ -423,7 +414,7 @@ export class Interpreter< * * This will also notify the `onStop` listeners. */ - public stop(): Interpreter { + public stop(): Interpreter { this.listeners.clear(); for (const listener of this.stopListeners) { // call listener, then remove @@ -463,7 +454,7 @@ export class Interpreter< } private _transition( - state: State, + state: State, event: SCXML.Event ) { try { @@ -631,7 +622,7 @@ export class Interpreter< */ public nextState( event: Event | SCXML.Event - ): State { + ): State { const _event = toSCXMLEvent(event); if ( @@ -769,7 +760,7 @@ export class Interpreter< } private exec( action: InvokeActionObject | BaseActionObject, - state: State + state: State ): void { const { _event } = state; @@ -844,17 +835,13 @@ export class Interpreter< }; } - public [symbolObservable](): Subscribable< - State - > { + public [symbolObservable](): Subscribable> { return this; } // this gets stripped by Babel to avoid having "undefined" property in environments without this non-standard Symbol // it has to be here to be included in the generated .d.ts - public [Symbol.observable](): Subscribable< - State - > { + public [Symbol.observable](): Subscribable> { return this; } @@ -874,16 +861,12 @@ export class Interpreter< */ export function interpret< TContext extends MachineContext, - TEvent extends EventObject = EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject = EventObject >( - machine: StateMachine, + machine: StateMachine, options?: Partial ) { - const interpreter = new Interpreter( - machine, - options - ); + const interpreter = new Interpreter(machine, options); return interpreter; } diff --git a/packages/core/src/invoke.ts b/packages/core/src/invoke.ts index d3ea09fb9f..f67d82d346 100644 --- a/packages/core/src/invoke.ts +++ b/packages/core/src/invoke.ts @@ -24,7 +24,7 @@ export const DEFAULT_SPAWN_OPTIONS = { sync: false }; export function invokeMachine< TContext extends MachineContext, TEvent extends EventObject, - TMachine extends StateMachine + TMachine extends StateMachine >( machine: | TMachine diff --git a/packages/core/src/model.types.ts b/packages/core/src/model.types.ts index f3bd350d85..6a3ab71fd7 100644 --- a/packages/core/src/model.types.ts +++ b/packages/core/src/model.types.ts @@ -8,7 +8,6 @@ import { MachineImplementations, BaseActionObject, MachineContext, - Typestate, DynamicAssignAction } from './types'; @@ -42,7 +41,7 @@ export interface Model< createMachine: ( config: MachineConfig, implementations?: Partial> - ) => StateMachine>; + ) => StateMachine; } export type ModelContextFrom< diff --git a/packages/core/src/stateUtils.ts b/packages/core/src/stateUtils.ts index 889a695946..1579c9c990 100644 --- a/packages/core/src/stateUtils.ts +++ b/packages/core/src/stateUtils.ts @@ -887,7 +887,7 @@ export function transitionNode< >( stateNode: StateNode, stateValue: StateValue, - state: State, + state: State, _event: SCXML.Event ): Transitions | undefined { // leaf node @@ -1487,7 +1487,7 @@ function selectEventlessTransitions< TContext extends MachineContext, TEvent extends EventObject >( - state: State, + state: State, machine: StateMachine ): Transitions { const enabledTransitions: Set< @@ -1534,9 +1534,9 @@ export function resolveMicroTransition< >( machine: StateMachine, transitions: Transitions, - currentState: State, + currentState: State, _event: SCXML.Event = initEvent as SCXML.Event -): State { +): State { // Transition will "apply" if: // - the state node is the initial state (there is no current state) // - OR there are transitions @@ -1646,9 +1646,9 @@ function resolveActionsAndContext< TEvent extends EventObject >( actions: BaseActionObject[], - machine: StateMachine, + machine: StateMachine, _event: SCXML.Event, - currentState: State | undefined + currentState: State | undefined ): { actions: typeof actions; raised: Array>; @@ -1865,7 +1865,7 @@ function resolveHistoryValue< TContext extends MachineContext, TEvent extends EventObject >( - currentState: State | undefined, + currentState: State | undefined, exitSet: Array> ): HistoryValue { const historyValue: Record< @@ -1907,7 +1907,7 @@ export function resolveStateValue< return getStateValue(rootNode, [...configuration]); } -export function toState>( +export function toState>( state: StateValue | TMachine['initialState'], machine: TMachine ): StateFromMachine { diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 730ef42749..b6c712aab2 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -826,11 +826,11 @@ export type HistoryValue< export type StateFrom< T extends - | StateMachine - | ((...args: any[]) => StateMachine) -> = T extends StateMachine + | StateMachine + | ((...args: any[]) => StateMachine) +> = T extends StateMachine ? ReturnType - : T extends (...args: any[]) => StateMachine + : T extends (...args: any[]) => StateMachine ? ReturnType['transition']> : never; @@ -1296,15 +1296,10 @@ export interface StateMeta< TContext extends MachineContext, TEvent extends EventObject > { - state: State; + state: State; _event: SCXML.Event; } -export interface Typestate { - value: StateValue; - context: TContext; -} - export interface StateLike { value: StateValue; context: TContext; @@ -1329,7 +1324,7 @@ export interface StateConfig< children: Record>; done?: boolean; tags?: Set; - machine?: StateMachine; + machine?: StateMachine; } export interface InterpreterOptions { @@ -1365,7 +1360,7 @@ export interface InterpreterOptions { execute?: boolean; } -export type AnyInterpreter = Interpreter; +export type AnyInterpreter = Interpreter; /** * Represents the `Interpreter` type of a given `StateMachine`. @@ -1373,9 +1368,9 @@ export type AnyInterpreter = Interpreter; * @typeParam TM - the machine to infer the interpreter's types from */ export type InterpreterOf< - TM extends StateMachine -> = TM extends StateMachine - ? Interpreter + TM extends StateMachine +> = TM extends StateMachine + ? Interpreter : never; export declare namespace SCXML { @@ -1444,7 +1439,7 @@ export declare namespace SCXML { // TODO: should only take in behaviors export type Spawnable = - | StateMachine + | StateMachine | PromiseLike | InvokeCallback | Subscribable @@ -1509,14 +1504,11 @@ export interface ActorRef export type ActorRefFrom = T extends StateMachine< infer TContext, - infer TEvent, - infer TTypestate + infer TEvent > - ? ActorRef> - : T extends ( - ...args: any[] - ) => StateMachine - ? ActorRef> + ? ActorRef> + : T extends (...args: any[]) => StateMachine + ? ActorRef> : T extends Promise ? ActorRef : T extends Behavior @@ -1532,14 +1524,12 @@ export type MaybeLazy = T | Lazy; export type InterpreterFrom< T extends - | StateMachine - | ((...args: any[]) => StateMachine) -> = T extends StateMachine - ? Interpreter - : T extends ( - ...args: any[] - ) => StateMachine - ? Interpreter + | StateMachine + | ((...args: any[]) => StateMachine) +> = T extends StateMachine + ? Interpreter + : T extends (...args: any[]) => StateMachine + ? Interpreter : never; export type EventOfMachine< @@ -1576,13 +1566,13 @@ export type EmittedFrom = ReturnTypeOrValue extends infer R : never; type ResolveEventType = ReturnTypeOrValue extends infer R - ? R extends StateMachine + ? R extends StateMachine ? TEvent : R extends Model ? TEvent - : R extends State + : R extends State ? TEvent - : R extends Interpreter + : R extends Interpreter ? TEvent : R extends ActorRef ? TEvent @@ -1603,13 +1593,13 @@ export type SimpleEventsOf< > = ExtractWithSimpleSupport; export type ContextFrom = ReturnTypeOrValue extends infer R - ? R extends StateMachine + ? R extends StateMachine ? TContext : R extends Model ? TContext - : R extends State + : R extends State ? TContext - : R extends Interpreter + : R extends Interpreter ? TContext : never : never; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 1860afe4d7..7c83d1681f 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -332,9 +332,7 @@ export const symbolObservable = (() => (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable')(); -export function isStateMachine( - value: any -): value is StateMachine { +export function isStateMachine(value: any): value is StateMachine { try { return '__xstatenode' in value && value.parent === undefined; } catch (e) { diff --git a/packages/core/test/interpreter.test.ts b/packages/core/test/interpreter.test.ts index 1bef3e52c3..38cee59230 100644 --- a/packages/core/test/interpreter.test.ts +++ b/packages/core/test/interpreter.test.ts @@ -87,7 +87,7 @@ describe('interpreter', () => { let entryCalled = 0; let promiseSpawned = 0; - const machine = createMachine({ + const machine = createMachine({ initial: 'idle', context: { actor: undefined @@ -1837,7 +1837,7 @@ describe('interpreter', () => { } }); - const formMachine = createMachine({ + const formMachine = createMachine({ id: 'form', initial: 'idle', context: {}, diff --git a/packages/core/test/match.test.ts b/packages/core/test/match.test.ts index 8412e679d7..2454047b54 100644 --- a/packages/core/test/match.test.ts +++ b/packages/core/test/match.test.ts @@ -1,4 +1,4 @@ -import { State, matchState, matchesState, createMachine, assign } from '../src'; +import { State, matchState, matchesState, createMachine } from '../src'; describe('matchState()', () => { it('should match a value from a pattern with the state (simple)', () => { @@ -302,134 +302,4 @@ describe('matches() method', () => { expect(machine.initialState.matches({ foo: 'bar' })).toBeTruthy(); expect(machine.initialState.matches('fake')).toBeFalsy(); }); - - it('should compile with typed matches (createMachine)', () => { - interface TestContext { - count?: number; - user?: { name: string }; - } - - type TestState = - | { - value: 'loading'; - context: { count: number; user: undefined }; - } - | { - value: 'loaded'; - context: { user: { name: string } }; - }; - - const machine = createMachine({ - initial: 'loading', - states: { - loading: { - initial: 'one', - states: { - one: {}, - two: {} - } - }, - loaded: {} - } - }); - - const init = machine.initialState; - - if (init.matches('loaded')) { - const name = init.context.user.name; - - // never called - it's okay if the name is undefined - expect(name).toBeTruthy(); - } else if (init.matches('loading')) { - // Make sure init isn't "never" - if it is, tests should fail to compile - expect(init).toBeTruthy(); - } - }); - - it('should compile with conditional matches even without a specified Typestate', () => { - const toggleMachine = createMachine<{ foo: number }>({ - id: 'toggle', - context: { - foo: 0 - }, - initial: 'a', - states: { - a: { on: { TOGGLE: 'b' } }, - b: { on: { TOGGLE: 'a' } } - } - }); - - const state = toggleMachine.initialState; - - if (state.matches('a') || state.matches('b')) { - // This should be a `number` type - state.context.foo; - - // Make sure state isn't "never" - if it is, tests should fail to compile - expect(state).toBeTruthy(); - } - }); - - it('should compile with a typestate value that is a union', (done) => { - interface MachineContext { - countObject: - | { - count: number; - } - | undefined; - } - - type MachineEvent = { type: 'TOGGLE' }; - - type MachineTypestate = - | { - value: 'active' | { other: 'one' }; - context: MachineContext & { countObject: { count: number } }; - } - | { - value: 'inactive'; - context: MachineContext; - }; - - const machine = createMachine< - MachineContext, - MachineEvent, - MachineTypestate - >({ - initial: 'active', - context: { - countObject: { count: 0 } - }, - states: { - inactive: { - entry: assign({ - countObject: undefined - }), - on: { TOGGLE: 'active' } - }, - active: { - entry: assign({ - countObject: (ctx) => ({ - count: (ctx.countObject?.count ?? 0) + 1 - }) - }), - on: { TOGGLE: 'other' } - }, - other: { - on: { TOGGLE: 'active' }, - initial: 'one', - states: { - one: {} - } - } - } - }); - - const state = machine.initialState; - - if (state.matches('active')) { - expect(state.context.countObject.count).toBe(1); - done(); - } - }); }); diff --git a/packages/core/test/transient.test.ts b/packages/core/test/transient.test.ts index f48f6d4e46..e21feb86f4 100644 --- a/packages/core/test/transient.test.ts +++ b/packages/core/test/transient.test.ts @@ -529,7 +529,7 @@ describe('transient states (eventless transitions)', () => { }); it('should work with transient transition on root', (done) => { - const machine = createMachine({ + const machine = createMachine({ id: 'machine', initial: 'first', context: { count: 0 }, @@ -565,7 +565,7 @@ describe('transient states (eventless transitions)', () => { }); it('should work with transient transition on root (with `always`)', (done) => { - const machine = createMachine({ + const machine = createMachine({ id: 'machine', initial: 'first', context: { count: 0 }, diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 20f6ac1a95..658a5b391b 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -1,4 +1,4 @@ -import { assign, createMachine, interpret } from '../src/index'; +import { assign, createMachine } from '../src/index'; import { raise } from '../src/actions/raise'; import { createModel } from '../src/model'; @@ -221,84 +221,6 @@ describe('Raise events', () => { }); }); -describe('Typestates', () => { - // Using "none" because undefined and null are unavailable when not in strict mode. - interface None { - type: 'none'; - } - const none: None = { type: 'none' }; - - const taskMachineConfiguration = { - id: 'task', - initial: 'idle', - context: { - result: none as None | number, - error: none as None | string - }, - states: { - idle: { - on: { RUN: 'running' } - }, - running: { - invoke: { - id: 'task-1', - src: 'taskService', - onDone: { target: 'succeeded', actions: 'assignSuccess' }, - onError: { target: 'failed', actions: 'assignFailure' } - } - }, - succeeded: {}, - failed: {} - } - }; - - type TaskContext = typeof taskMachineConfiguration.context; - - type TaskTypestate = - | { value: 'idle'; context: { result: None; error: None } } - | { value: 'running'; context: { result: None; error: None } } - | { value: 'succeeded'; context: { result: number; error: None } } - | { value: 'failed'; context: { result: None; error: string } }; - - type ExtractTypeState = Extract< - TaskTypestate, - { value: T } - >['context']; - type Idle = ExtractTypeState<'idle'>; - type Running = ExtractTypeState<'running'>; - type Succeeded = ExtractTypeState<'succeeded'>; - type Failed = ExtractTypeState<'failed'>; - - const machine = createMachine( - taskMachineConfiguration - ); - - it("should preserve typestate for the service returned by Interpreter.start() and a servcie's .state getter.", () => { - const service = interpret(machine); - const startedService = service.start(); - - const idle: Idle = startedService.state.matches('idle') - ? startedService.state.context - : { result: none, error: none }; - expect(idle).toEqual({ result: none, error: none }); - - const running: Running = startedService.state.matches('running') - ? startedService.state.context - : { result: none, error: none }; - expect(running).toEqual({ result: none, error: none }); - - const succeeded: Succeeded = startedService.state.matches('succeeded') - ? startedService.state.context - : { result: 12, error: none }; - expect(succeeded).toEqual({ result: 12, error: none }); - - const failed: Failed = startedService.state.matches('failed') - ? startedService.state.context - : { result: none, error: 'oops' }; - expect(failed).toEqual({ result: none, error: 'oops' }); - }); -}); - describe('types', () => { it('defined context in createMachine() should be an object', () => { createMachine({ diff --git a/packages/xstate-graph/src/graph.ts b/packages/xstate-graph/src/graph.ts index 9b7a624ae5..02a45d412c 100644 --- a/packages/xstate-graph/src/graph.ts +++ b/packages/xstate-graph/src/graph.ts @@ -37,7 +37,7 @@ const EMPTY_MAP = {}; * @param stateNode State node to recursively get child state nodes from */ export function getStateNodes( - stateNode: StateNode | StateMachine + stateNode: StateNode | StateMachine ): StateNode[] { const { states } = stateNode; const nodes = keys(states).reduce((accNodes: StateNode[], stateKey) => { diff --git a/packages/xstate-inspect/src/inspectMachine.ts b/packages/xstate-inspect/src/inspectMachine.ts index 75e0d8fd12..8aa6bd4d36 100644 --- a/packages/xstate-inspect/src/inspectMachine.ts +++ b/packages/xstate-inspect/src/inspectMachine.ts @@ -14,7 +14,7 @@ export function createInspectMachine( devTools: XStateDevInterface = globalThis.__xstate__, options?: { serialize?: Replacer | undefined } ) { - const serviceMap = new Map>(); + const serviceMap = new Map>(); // Listen for services being registered and index them // by their sessionId for quicker lookup diff --git a/packages/xstate-inspect/src/serialize.ts b/packages/xstate-inspect/src/serialize.ts index b8993262ba..4b7889f4dc 100644 --- a/packages/xstate-inspect/src/serialize.ts +++ b/packages/xstate-inspect/src/serialize.ts @@ -28,7 +28,7 @@ export function stringifyState( } export function stringifyMachine( - machine: StateMachine, + machine: StateMachine, replacer?: Replacer ): string { return selectivelyStringify(machine, ['context'], replacer); diff --git a/packages/xstate-inspect/src/types.ts b/packages/xstate-inspect/src/types.ts index cb8bb55c12..3ce6fb6659 100644 --- a/packages/xstate-inspect/src/types.ts +++ b/packages/xstate-inspect/src/types.ts @@ -16,7 +16,7 @@ export interface InspectorOptions { } export interface Inspector - extends ActorRef> { + extends ActorRef> { /** * Disconnects the inspector. */ diff --git a/packages/xstate-react/src/useInterpret.ts b/packages/xstate-react/src/useInterpret.ts index 459e97fa5f..0261893dab 100644 --- a/packages/xstate-react/src/useInterpret.ts +++ b/packages/xstate-react/src/useInterpret.ts @@ -8,7 +8,6 @@ import { Interpreter, InterpreterOptions, MachineImplementations, - Typestate, Observer } from 'xstate'; import { MachineContext } from '../../core/src'; @@ -39,17 +38,16 @@ function toObserver( export function useInterpret< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject >( - getMachine: MaybeLazy>, + getMachine: MaybeLazy>, options: Partial & Partial> & Partial> = {}, observerOrListener?: - | Observer> - | ((value: State) => void) -): Interpreter { + | Observer> + | ((value: State) => void) +): Interpreter { const machine = useConstant(() => { return typeof getMachine === 'function' ? getMachine() : getMachine; }); diff --git a/packages/xstate-react/src/useMachine.ts b/packages/xstate-react/src/useMachine.ts index 9f25df75c5..a4b9f53211 100644 --- a/packages/xstate-react/src/useMachine.ts +++ b/packages/xstate-react/src/useMachine.ts @@ -6,7 +6,6 @@ import { InterpreterOptions, MachineImplementations, StateConfig, - Typestate, ActionFunction, InterpreterOf, MachineContext @@ -69,35 +68,30 @@ export interface UseMachineOptions< export function useMachine< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject >( - getMachine: MaybeLazy>, + getMachine: MaybeLazy>, options: Partial & Partial> & Partial> = {} ): [ - State, - InterpreterOf>['send'], - InterpreterOf> + State, + InterpreterOf>['send'], + InterpreterOf> ] { - const listener = useCallback( - (nextState: State) => { - // Only change the current state if: - // - the incoming state is the "live" initial state (since it might have new actors) - // - OR the incoming state actually changed. - // - // The "live" initial state will have .changed === undefined. - const initialStateChanged = - nextState.changed === undefined && - Object.keys(nextState.children).length; + const listener = useCallback((nextState: State) => { + // Only change the current state if: + // - the incoming state is the "live" initial state (since it might have new actors) + // - OR the incoming state actually changed. + // + // The "live" initial state will have .changed === undefined. + const initialStateChanged = + nextState.changed === undefined && Object.keys(nextState.children).length; - if (nextState.changed || initialStateChanged) { - setState(nextState); - } - }, - [] - ); + if (nextState.changed || initialStateChanged) { + setState(nextState); + } + }, []); const service = useInterpret(getMachine, options, listener); @@ -105,7 +99,7 @@ export function useMachine< const { initialState } = service.machine; return (options.state ? State.create(options.state) - : initialState) as State; + : initialState) as State; }); return [state, service.send, service]; diff --git a/packages/xstate-react/src/useReactEffectActions.ts b/packages/xstate-react/src/useReactEffectActions.ts index d0d7e8e5dd..7d24862483 100644 --- a/packages/xstate-react/src/useReactEffectActions.ts +++ b/packages/xstate-react/src/useReactEffectActions.ts @@ -9,7 +9,7 @@ function executeEffect< TEvent extends EventObject >( action: ReactActionObject, - state: State + state: State ): void { const { exec } = action.params; if (!exec) { @@ -27,12 +27,12 @@ function executeEffect< export function useReactEffectActions< TContext extends MachineContext, TEvent extends EventObject ->(service: Interpreter) { +>(service: Interpreter) { const effectActionsRef = useRef< - Array<[ReactActionObject, State]> + Array<[ReactActionObject, State]> >([]); const layoutEffectActionsRef = useRef< - Array<[ReactActionObject, State]> + Array<[ReactActionObject, State]> >([]); useIsomorphicLayoutEffect(() => { diff --git a/packages/xstate-react/src/useSelector.ts b/packages/xstate-react/src/useSelector.ts index 42a1ae8403..f6f5315d05 100644 --- a/packages/xstate-react/src/useSelector.ts +++ b/packages/xstate-react/src/useSelector.ts @@ -5,7 +5,7 @@ import { ActorRef, Interpreter, Subscribable } from 'xstate'; import { isActorWithState } from './useActor'; import { getServiceSnapshot } from './useService'; -function isService(actor: any): actor is Interpreter { +function isService(actor: any): actor is Interpreter { return 'state' in actor && 'machine' in actor; } diff --git a/packages/xstate-react/src/useService.ts b/packages/xstate-react/src/useService.ts index 07e6caaa14..85096ff4ec 100644 --- a/packages/xstate-react/src/useService.ts +++ b/packages/xstate-react/src/useService.ts @@ -1,14 +1,8 @@ -import { - EventObject, - State, - Interpreter, - Typestate, - MachineContext -} from 'xstate'; +import { EventObject, State, Interpreter, MachineContext } from 'xstate'; import { useActor } from './useActor'; import { PayloadSender } from './types'; -export function getServiceSnapshot>( +export function getServiceSnapshot>( service: TService ): TService['state'] { // TODO: remove compat lines in a new major, replace literal number with InterpreterStatus then as well @@ -25,11 +19,10 @@ export function getServiceSnapshot>( */ export function useService< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject >( - service: Interpreter -): [State, PayloadSender] { + service: Interpreter +): [State, PayloadSender] { if (process.env.NODE_ENV !== 'production' && !('machine' in service)) { throw new Error( `Attempted to use an actor-like object instead of a service in the useService() hook. Please use the useActor() hook instead.` diff --git a/packages/xstate-react/test/types.test.tsx b/packages/xstate-react/test/types.test.tsx index fdacf30d2b..f9bee900ef 100644 --- a/packages/xstate-react/test/types.test.tsx +++ b/packages/xstate-react/test/types.test.tsx @@ -73,11 +73,11 @@ describe('useMachine', () => { type: 'YES'; } - type YesNoTypestate = - | { value: 'no'; context: { value: undefined } } - | { value: 'yes'; context: { value: number } }; + // type YesNoTypestate = + // | { value: 'no'; context: { value: undefined } } + // | { value: 'yes'; context: { value: number } }; - const yesNoMachine = createMachine({ + const yesNoMachine = createMachine({ context: { value: undefined }, @@ -94,26 +94,6 @@ describe('useMachine', () => { } }); - it('should preserve typestate information.', () => { - const YesNo = () => { - const [state] = useMachine(yesNoMachine); - - if (state.matches('no')) { - const undefinedValue: undefined = state.context.value; - - return {undefinedValue ? 'Yes' : 'No'}; - } else if (state.matches('yes')) { - const numericValue: number = state.context.value; - - return {numericValue ? 'Yes' : 'No'}; - } - - return No; - }; - - render(); - }); - it('state should not become never after checking state with matches', () => { const YesNo = () => { const [state] = useMachine(yesNoMachine); diff --git a/packages/xstate-react/test/useMachine.test.tsx b/packages/xstate-react/test/useMachine.test.tsx index b290c72103..8a300eaf62 100644 --- a/packages/xstate-react/test/useMachine.test.tsx +++ b/packages/xstate-react/test/useMachine.test.tsx @@ -292,17 +292,17 @@ describe('useMachine hook', () => { user?: { name: string }; } - type TestState = - | { - value: 'loading'; - context: { count: number; user: undefined }; - } - | { - value: 'loaded'; - context: { user: { name: string } }; - }; - - const machine = createMachine({ + // type TestState = + // | { + // value: 'loading'; + // context: { count: number; user: undefined }; + // } + // | { + // value: 'loaded'; + // context: { user: { name: string } }; + // }; + + const machine = createMachine({ initial: 'loading', states: { loading: { @@ -317,12 +317,12 @@ describe('useMachine hook', () => { }); const ServiceApp: React.FC<{ - service: Interpreter; + service: Interpreter; }> = ({ service }) => { const [state] = useService(service); if (state.matches('loaded')) { - const name = state.context.user.name; + const name = state.context.user!.name; // never called - it's okay if the name is undefined expect(name).toBeTruthy(); @@ -338,7 +338,7 @@ describe('useMachine hook', () => { const [state, , service] = useMachine(machine); if (state.matches('loaded')) { - const name = state.context.user.name; + const name = state.context.user!.name; // never called - it's okay if the name is undefined expect(name).toBeTruthy(); diff --git a/packages/xstate-scxml/src/index.ts b/packages/xstate-scxml/src/index.ts index d65afced41..f26530f7cd 100644 --- a/packages/xstate-scxml/src/index.ts +++ b/packages/xstate-scxml/src/index.ts @@ -153,7 +153,7 @@ function stateNodeToSCXML(stateNode: StateNode): XMLElement { }; } -export function toSCXML(machine: StateMachine): string { +export function toSCXML(machine: StateMachine): string { const { states, initial } = machine.root; const elements = Object.keys(states).map((key) => { diff --git a/packages/xstate-svelte/src/useMachine.ts b/packages/xstate-svelte/src/useMachine.ts index a5b87175aa..67b8700f55 100644 --- a/packages/xstate-svelte/src/useMachine.ts +++ b/packages/xstate-svelte/src/useMachine.ts @@ -5,8 +5,7 @@ import { StateMachine, InterpreterOptions, MachineImplementations, - StateConfig, - Typestate + StateConfig } from 'xstate'; interface UseMachineOptions< @@ -24,12 +23,8 @@ interface UseMachineOptions< state: StateConfig; } -export function useMachine< - TContext extends object, - TEvent extends EventObject, - TTypestate extends Typestate ->( - machine: StateMachine, +export function useMachine( + machine: StateMachine, options: Partial & Partial> & Partial> = {} diff --git a/packages/xstate-test/src/index.ts b/packages/xstate-test/src/index.ts index e7a0e227a1..382944de39 100644 --- a/packages/xstate-test/src/index.ts +++ b/packages/xstate-test/src/index.ts @@ -58,7 +58,7 @@ export class TestModel { }; constructor( - public machine: StateMachine, + public machine: StateMachine, options?: Partial> ) { this.options = { @@ -467,7 +467,7 @@ function getEventSamples(eventsOptions: TestModelOptions['events']) { * to an event test config (e.g., `{exec: () => {...}, cases: [...]}`) */ export function createModel( - machine: StateMachine, + machine: StateMachine, options?: TestModelOptions ): TestModel { return new TestModel(machine, options); diff --git a/packages/xstate-vue/src/useInterpret.ts b/packages/xstate-vue/src/useInterpret.ts index 4ebff1f853..e4482efadc 100644 --- a/packages/xstate-vue/src/useInterpret.ts +++ b/packages/xstate-vue/src/useInterpret.ts @@ -5,7 +5,6 @@ import { Interpreter, InterpreterOptions, MachineImplementations, - Typestate, Observer, StateMachine } from 'xstate'; @@ -35,17 +34,16 @@ function toObserver( export function useInterpret< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject >( - getMachine: MaybeLazy>, + getMachine: MaybeLazy>, options: Partial & Partial> & Partial> = {}, observerOrListener?: - | Observer> - | ((value: State) => void) -): Interpreter { + | Observer> + | ((value: State) => void) +): Interpreter { const machine = typeof getMachine === 'function' ? getMachine() : getMachine; const { diff --git a/packages/xstate-vue/src/useMachine.ts b/packages/xstate-vue/src/useMachine.ts index bdaf011060..823261ce8d 100644 --- a/packages/xstate-vue/src/useMachine.ts +++ b/packages/xstate-vue/src/useMachine.ts @@ -6,7 +6,6 @@ import { Interpreter, InterpreterOptions, MachineImplementations, - Typestate, MachineContext } from 'xstate'; @@ -16,17 +15,16 @@ import { useInterpret } from './useInterpret'; export function useMachine< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject >( - getMachine: MaybeLazy>, + getMachine: MaybeLazy>, options: Partial & Partial> & Partial> = {} ): { - state: Ref>; - send: Interpreter['send']; - service: Interpreter; + state: Ref>; + send: Interpreter['send']; + service: Interpreter; } { const service = useInterpret(getMachine, options, listener); @@ -34,12 +32,11 @@ export function useMachine< const state = shallowRef( (options.state ? State.create(options.state) : initialState) as State< TContext, - TEvent, - TTypestate + TEvent > ); - function listener(nextState: State) { + function listener(nextState: State) { // Only change the current state if: // - the incoming state is the "live" initial state (since it might have new actors) // - OR the incoming state actually changed. diff --git a/packages/xstate-vue/src/useService.ts b/packages/xstate-vue/src/useService.ts index 687cca08ae..0a415785c4 100644 --- a/packages/xstate-vue/src/useService.ts +++ b/packages/xstate-vue/src/useService.ts @@ -2,7 +2,6 @@ import { EventObject, State, Interpreter, - Typestate, PayloadSender, MachineContext } from 'xstate'; @@ -19,14 +18,11 @@ import { useActor } from './useActor'; */ export function useService< TContext extends MachineContext, - TEvent extends EventObject, - TTypestate extends Typestate = { value: any; context: TContext } + TEvent extends EventObject >( - service: - | Interpreter - | Ref> + service: Interpreter | Ref> ): { - state: Ref>; + state: Ref>; send: PayloadSender; } { if ( From c99bb43afec01ddee86fc746c346ea1aeeca687d Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Sun, 12 Dec 2021 07:24:41 -0700 Subject: [PATCH 2/4] Add changeset --- .changeset/small-papayas-wait.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/small-papayas-wait.md diff --git a/.changeset/small-papayas-wait.md b/.changeset/small-papayas-wait.md new file mode 100644 index 0000000000..2a1a40237d --- /dev/null +++ b/.changeset/small-papayas-wait.md @@ -0,0 +1,5 @@ +--- +'xstate': major +--- + +Typings for `Typestate` have been removed. The reason for this is that types for typestates needed to be manually specified, which is unsound because it is possible to specify _impossible_ typestates; i.e., typings for a state's `value` and `context` that are impossible to achieve. From 79b75945e0ee5516cf74e3caa20f50dd35f663d7 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 15 Dec 2021 04:09:32 -0800 Subject: [PATCH 3/4] Update packages/xstate-react/test/types.test.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-react/test/types.test.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/xstate-react/test/types.test.tsx b/packages/xstate-react/test/types.test.tsx index f9bee900ef..39ceaddb64 100644 --- a/packages/xstate-react/test/types.test.tsx +++ b/packages/xstate-react/test/types.test.tsx @@ -73,10 +73,6 @@ describe('useMachine', () => { type: 'YES'; } - // type YesNoTypestate = - // | { value: 'no'; context: { value: undefined } } - // | { value: 'yes'; context: { value: number } }; - const yesNoMachine = createMachine({ context: { value: undefined From b00e06565ca4a99e2e2e4009c1131e04ac1744a4 Mon Sep 17 00:00:00 2001 From: David Khourshid Date: Wed, 15 Dec 2021 04:09:38 -0800 Subject: [PATCH 4/4] Update packages/xstate-react/test/useMachine.test.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Burzyński --- packages/xstate-react/test/useMachine.test.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/xstate-react/test/useMachine.test.tsx b/packages/xstate-react/test/useMachine.test.tsx index 8a300eaf62..c7620b651d 100644 --- a/packages/xstate-react/test/useMachine.test.tsx +++ b/packages/xstate-react/test/useMachine.test.tsx @@ -292,16 +292,6 @@ describe('useMachine hook', () => { user?: { name: string }; } - // type TestState = - // | { - // value: 'loading'; - // context: { count: number; user: undefined }; - // } - // | { - // value: 'loaded'; - // context: { user: { name: string } }; - // }; - const machine = createMachine({ initial: 'loading', states: {