Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v5] Restrict context to object #2294

Merged
merged 8 commits into from
Jun 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .changeset/cold-steaks-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
'xstate': major
---

The machine's `context` is now restricted to an `object`. This was the most common usage, but now the typings prevent `context` from being anything but an object:

```ts
const machine = createMachine({
// This will produce the TS error:
// "Type 'string' is not assignable to type 'object | undefined'"
context: 'some string'
});
```

If `context` is `undefined`, it will now default to an empty object `{}`:

```ts
const machine = createMachine({
// No context
});

machine.initialState.context;
// => {}
```
8 changes: 6 additions & 2 deletions packages/core/src/Actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
InterpreterOptions,
ActorRef,
Lazy,
BaseActorRef
BaseActorRef,
MachineContext
} from './types';
import { StateMachine } from './StateMachine';
import { State } from './State';
Expand Down Expand Up @@ -68,7 +69,10 @@ export function fromCallback<TEvent extends EventObject>(
);
}

export function fromMachine<TContext, TEvent extends EventObject>(
export function fromMachine<
TContext extends MachineContext,
TEvent extends EventObject
>(
machine: StateMachine<TContext, TEvent>,
name: string,
options?: Partial<InterpreterOptions>
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/Machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,31 @@ import {
MachineConfig,
EventObject,
AnyEventObject,
Typestate
Typestate,
MachineContext
} from './types';
import { StateMachine } from './StateMachine';
import { Model, ModelContextFrom, ModelEventsFrom } from './model';

export function createMachine<
TModel extends Model<any, any, any>,
TContext = ModelContextFrom<TModel>,
TContext extends MachineContext = ModelContextFrom<TModel>,
TEvent extends EventObject = ModelEventsFrom<TModel>,
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
>(
config: MachineConfig<TContext, TEvent>,
options?: Partial<MachineImplementations<TContext, TEvent>>
): StateMachine<TContext, TEvent, TTypestate>;
export function createMachine<
TContext,
TContext extends MachineContext,
TEvent extends EventObject = AnyEventObject,
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
>(
config: MachineConfig<TContext, TEvent>,
options?: Partial<MachineImplementations<TContext, TEvent>>
): StateMachine<TContext, TEvent, TTypestate>;
export function createMachine<
TContext,
TContext extends MachineContext,
TEvent extends EventObject = AnyEventObject,
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
>(
Expand Down
58 changes: 33 additions & 25 deletions packages/core/src/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import {
Typestate,
HistoryValue,
NullEvent,
ActorRef
ActorRef,
MachineContext
} from './types';
import { matchesState, keys, isString } from './utils';
import { StateNode } from './StateNode';
import { isInFinalState, nextEvents, getMeta } from './stateUtils';
import { initEvent } from './actions';

export function isState<
TContext,
TContext extends MachineContext,
TEvent extends EventObject,
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
>(state: object | string): state is State<TContext, TEvent, TTypestate> {
Expand All @@ -27,17 +28,20 @@ export function isState<

return 'value' in state && 'history' in state;
}
export function bindActionToState<TC, TE extends EventObject>(
action: ActionObject<TC, TE>,
state: State<TC, TE, any>
): ActionObject<TC, TE> {
export function bindActionToState<
TContext extends MachineContext,
TEvent extends EventObject
>(
action: ActionObject<TContext, TEvent>,
state: State<TContext, TEvent, any>
): ActionObject<TContext, TEvent> {
const { exec } = action;
const boundAction: ActionObject<TC, TE> = {
const boundAction: ActionObject<TContext, TEvent> = {
...action,
exec:
exec !== undefined
? () =>
exec(state.context, state.event as TE, {
exec(state.context, state.event as TEvent, {
action,
state,
_event: state._event
Expand All @@ -49,7 +53,7 @@ export function bindActionToState<TC, TE extends EventObject>(
}

export class State<
TContext,
TContext extends MachineContext,
TEvent extends EventObject = EventObject,
TTypestate extends Typestate<TContext> = { value: any; context: TContext }
> {
Expand Down Expand Up @@ -95,15 +99,18 @@ export class State<
* @param stateValue
* @param context
*/
public static from<TC, TE extends EventObject = EventObject>(
stateValue: State<TC, TE, any> | StateValue,
context?: TC | undefined
): State<TC, TE, any> {
public static from<
TContext extends MachineContext,
TEvent extends EventObject = EventObject
>(
stateValue: State<TContext, TEvent, any> | StateValue,
context: TContext = {} as TContext
): State<TContext, TEvent, any> {
if (stateValue instanceof State) {
if (stateValue.context !== context) {
return new State<TC, TE>({
return new State<TContext, TEvent>({
value: stateValue.value,
context: context as TC,
context: context as TContext,
_event: stateValue._event,
_sessionid: null,
history: stateValue.history,
Expand All @@ -118,11 +125,11 @@ export class State<
return stateValue;
}

const _event = initEvent as SCXML.Event<TE>;
const _event = initEvent as SCXML.Event<TEvent>;

return new State<TC, TE>({
return new State<TContext, TEvent>({
value: stateValue,
context: context as TC,
context: context as TContext,
_event,
_sessionid: null,
history: undefined,
Expand All @@ -137,9 +144,10 @@ export class State<
* Creates a new State instance for the given `config`.
* @param config The state config
*/
public static create<TC, TE extends EventObject = EventObject>(
config: StateConfig<TC, TE>
): State<TC, TE, any> {
public static create<
TContext extends MachineContext,
TEvent extends EventObject = EventObject
>(config: StateConfig<TContext, TEvent>): State<TContext, TEvent, any> {
return new State(config);
}
/**
Expand All @@ -151,10 +159,10 @@ export class State<
state: TState,
context: any
): TState;
public static inert<TC, TE extends EventObject = EventObject>(
stateValue: StateValue,
context: TC
): State<TC, TE>;
public static inert<
TContext extends MachineContext,
TEvent extends EventObject = EventObject
>(stateValue: StateValue, context: TContext): State<TContext, TEvent>;
public static inert(
stateValue: State<any, any> | StateValue,
context: any
Expand Down
16 changes: 5 additions & 11 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
Typestate,
Transitions,
MachineSchema,
StateNodeDefinition
StateNodeDefinition,
MachineContext
} from './types';
import { State } from './State';

Expand All @@ -30,7 +31,7 @@ export const NULL_EVENT = '';
export const STATE_IDENTIFIER = '#';
export const WILDCARD = '*';

const createDefaultOptions = <TContext>(
const createDefaultOptions = <TContext extends MachineContext>(
context: TContext
): MachineImplementations<TContext, any> => ({
actions: {},
Expand All @@ -44,18 +45,14 @@ function resolveContext<TContext>(
context: TContext,
partialContext?: Partial<TContext>
): TContext {
if (context === undefined) {
return context;
}

return {
...context,
...partialContext
};
}

export class StateMachine<
TContext = any,
TContext extends MachineContext = any,
TEvent extends EventObject = EventObject,
TTypestate extends Typestate<TContext> = any
> {
Expand Down Expand Up @@ -100,10 +97,7 @@ export class StateMachine<
createDefaultOptions(config.context!),
options
);
this.context = resolveContext<TContext>(
config.context as TContext,
this.options.context
);
this.context = resolveContext(config.context, options?.context) as TContext;
this.delimiter = this.config.delimiter || STATE_DELIMITER;
this.version = this.config.version;
this.schema = this.config.schema ?? (({} as any) as this['schema']);
Expand Down
10 changes: 7 additions & 3 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
NullEvent,
SCXML,
TransitionDefinitionMap,
InitialTransitionDefinition
InitialTransitionDefinition,
MachineContext
} from './types';
import { State } from './State';
import * as actionTypes from './actionTypes';
Expand All @@ -44,14 +45,17 @@ import { StateMachine } from './StateMachine';

const EMPTY_OBJECT = {};

interface StateNodeOptions<TContext, TEvent extends EventObject> {
interface StateNodeOptions<
TContext extends MachineContext,
TEvent extends EventObject
> {
_key: string;
_parent?: StateNode<TContext, TEvent>;
_machine: StateMachine<TContext, TEvent>;
}

export class StateNode<
TContext = any,
TContext extends MachineContext = MachineContext,
TEvent extends EventObject = EventObject
> {
/**
Expand Down
Loading