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] Action types #4180

Merged
merged 15 commits into from
Aug 22, 2023
26 changes: 26 additions & 0 deletions .changeset/sixty-rules-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
'xstate': minor
---

You can now specify action types for machines:

```ts
createMachine({
types: {} as {
actions: { type: 'greet'; params: { name: string } };
},
entry: [
{
type: 'greet',
params: {
name: 'David'
}
},
// @ts-expect-error
{ type: 'greet' },
// @ts-expect-error
{ type: 'unknownAction' }
]
// ...
});
```
11 changes: 6 additions & 5 deletions packages/core/src/Machine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ export function createMachine<
TActor extends ProvidedActor = ProvidedActor,
TInput = any,
TOutput = NonReducibleUnknown,
TAction extends ParameterizedObject = ParameterizedObject,
TTypesMeta extends TypegenConstraint = TypegenDisabled
>(
config: MachineConfig<
TContext,
TEvent,
ParameterizedObject,
TAction,
TActor,
TInput,
TOutput,
Expand All @@ -35,18 +36,18 @@ export function createMachine<
implementations?: InternalMachineImplementations<
TContext,
TEvent,
ParameterizedObject,
TAction,
TActor,
ResolveTypegenMeta<TTypesMeta, TEvent, ParameterizedObject, TActor>
ResolveTypegenMeta<TTypesMeta, TEvent, TAction, TActor>
>
): StateMachine<
TContext,
TEvent,
ParameterizedObject,
TAction,
TActor,
TInput,
TOutput,
ResolveTypegenMeta<TTypesMeta, TEvent, ParameterizedObject, TActor>
ResolveTypegenMeta<TTypesMeta, TEvent, TAction, TActor>
> {
return new StateMachine<any, any, any, any, any, any, any>(
config as any,
Expand Down
15 changes: 12 additions & 3 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,14 @@ export class StateMachine<

public implementations: MachineImplementationsSimplified<TContext, TEvent>;

public types: MachineTypes<TContext, TEvent, TActor, TInput, TOutput>;
public types: MachineTypes<
TContext,
TEvent,
TAction,
TActor,
TInput,
TOutput
>;

public __xstatenode: true = true;

Expand Down Expand Up @@ -465,7 +472,9 @@ export class StateMachine<
/** @deprecated an internal property acting as a "phantom" type, not meant to be used at runtime */
__TActor!: TActor;
/** @deprecated an internal property acting as a "phantom" type, not meant to be used at runtime */
__TResolvedTypesMeta!: TResolvedTypesMeta;

__TInput!: TInput;
/** @deprecated an internal property acting as a "phantom" type, not meant to be used at runtime */
__TOutput!: TOutput;
/** @deprecated an internal property acting as a "phantom" type, not meant to be used at runtime */
__TResolvedTypesMeta!: TResolvedTypesMeta;
}
32 changes: 19 additions & 13 deletions packages/core/src/StateNode.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add TAction to StateNode as well - but I kinda feel that state nodes should mostly be treated as internal data structures. Even if we expose them... people should just treat things as unknown etc as that's mostly useful for inspection, analysis etc and that kind of code is super generic. Thoughts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that StateNode should be internal, so it's your call whether to add it or not. Can always be added later

Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
getDelayedTransitions
} from './stateUtils.ts';
import type {
Action,
AnyActorLogic,
DelayedTransitionDefinition,
EventObject,
Expand All @@ -21,14 +20,15 @@ import type {
InvokeDefinition,
MachineContext,
Mapper,
PropertyMapper,
StateNodeConfig,
StateNodeDefinition,
StateNodesConfig,
StatesDefinition,
TransitionDefinition,
TransitionDefinitionMap,
TODO
TODO,
UnknownAction,
ParameterizedObject
} from './types.ts';
import {
createInvokeId,
Expand All @@ -41,7 +41,7 @@ import {

const EMPTY_OBJECT = {};

const toSerializableActon = (action: Action<any, any, any>) => {
const toSerializableActon = (action: UnknownAction) => {
if (typeof action === 'string') {
return { type: action };
}
Expand Down Expand Up @@ -105,11 +105,11 @@ export class StateNode<
/**
* The action(s) to be executed upon entering the state node.
*/
public entry: Action<any, any, any>[];
public entry: UnknownAction[];
/**
* The action(s) to be executed upon exiting the state node.
*/
public exit: Action<any, any, any>[];
public exit: UnknownAction[];
/**
* The parent state node.
*/
Expand Down Expand Up @@ -196,15 +196,15 @@ export class StateNode<
this.history =
this.config.history === true ? 'shallow' : this.config.history || false;

this.entry = toArray(this.config.entry);
this.exit = toArray(this.config.exit);
this.entry = toArray(this.config.entry).slice();
this.exit = toArray(this.config.exit).slice();

this.meta = this.config.meta;
this.output =
this.type === 'final'
? (this.config as FinalStateNodeConfig<TContext, TEvent>).output
: undefined;
this.tags = toArray(config.tags);
this.tags = toArray(config.tags).slice();
}

public _initialize() {
Expand Down Expand Up @@ -271,7 +271,9 @@ export class StateNode<
/**
* The logic invoked as actors by this state node.
*/
public get invoke(): Array<InvokeDefinition<TContext, TEvent>> {
public get invoke(): Array<
InvokeDefinition<TContext, TEvent, ParameterizedObject>
> {
return memo(this, 'invoke', () =>
toArray(this.config.invoke).map((invocable, i) => {
const generatedId = createInvokeId(this.id, i);
Expand Down Expand Up @@ -310,7 +312,7 @@ export class StateNode<
id: resolvedId
};
}
} as InvokeDefinition<TContext, TEvent>;
} as InvokeDefinition<TContext, TEvent, ParameterizedObject>;
})
);
}
Expand All @@ -333,7 +335,11 @@ export class StateNode<
}

public get after(): Array<DelayedTransitionDefinition<TContext, TEvent>> {
return memo(this, 'delayedTransitions', () => getDelayedTransitions(this));
return memo(
this,
'delayedTransitions',
() => getDelayedTransitions(this) as any
);
}

public get initial(): InitialTransitionDefinition<TContext, TEvent> {
Expand All @@ -347,7 +353,7 @@ export class StateNode<
event: TEvent
): TransitionDefinition<TContext, TEvent>[] | undefined {
const eventType = event.type;
const actions: Action<any, any, any>[] = [];
const actions: UnknownAction[] = [];

let selectedTransition: TransitionDefinition<TContext, TEvent> | undefined;

Expand Down
19 changes: 12 additions & 7 deletions packages/core/src/actions/assign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ import type {
EventObject,
LowInfer,
MachineContext,
ParameterizedObject,
PropertyAssigner
} from '../types.ts';

function resolve(
actorContext: AnyActorContext,
state: AnyState,
actionArgs: ActionArgs<any, any>,
actionArgs: ActionArgs<any, any, any>,
{
assignment
}: {
assignment: Assigner<any, any> | PropertyAssigner<any, any>;
assignment: Assigner<any, any, any> | PropertyAssigner<any, any, any>;
}
) {
if (!state.context) {
Expand All @@ -31,7 +32,7 @@ function resolve(
}
const spawnedChildren: Record<string, AnyActorRef> = {};

const assignArgs: AssignArgs<any, any> = {
const assignArgs: AssignArgs<any, any, any> = {
context: state.context,
event: actionArgs.event,
action: actionArgs.action,
Expand Down Expand Up @@ -80,13 +81,17 @@ function resolve(
export function assign<
TContext extends MachineContext,
TExpressionEvent extends EventObject = EventObject,
TEvent extends EventObject = TExpressionEvent
TExpressionAction extends ParameterizedObject | undefined =
| ParameterizedObject
| undefined
>(
assignment:
| Assigner<LowInfer<TContext>, TExpressionEvent>
| PropertyAssigner<LowInfer<TContext>, TExpressionEvent>
| Assigner<LowInfer<TContext>, TExpressionEvent, TExpressionAction>
| PropertyAssigner<LowInfer<TContext>, TExpressionEvent, TExpressionAction>
) {
function assign(_: ActionArgs<TContext, TExpressionEvent>) {
function assign(
_: ActionArgs<TContext, TExpressionEvent, TExpressionAction>
) {
if (isDevelopment) {
throw new Error(`This isn't supposed to be called`);
}
Expand Down
24 changes: 16 additions & 8 deletions packages/core/src/actions/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,25 @@ import {
AnyState,
EventObject,
MachineContext,
ActionArgs
ActionArgs,
ParameterizedObject
} from '../types.ts';

type ResolvableSendId<
TContext extends MachineContext,
TExpressionEvent extends EventObject
> = string | ((args: ActionArgs<TContext, TExpressionEvent>) => string);
TExpressionEvent extends EventObject,
TExpressionAction extends ParameterizedObject | undefined
> =
| string
| ((
args: ActionArgs<TContext, TExpressionEvent, TExpressionAction>
) => string);

function resolve(
_: AnyActorContext,
state: AnyState,
actionArgs: ActionArgs<any, any>,
{ sendId }: { sendId: ResolvableSendId<any, any> }
actionArgs: ActionArgs<any, any, any>,
{ sendId }: { sendId: ResolvableSendId<any, any, any> }
) {
const resolvedSendId =
typeof sendId === 'function' ? sendId(actionArgs) : sendId;
Expand All @@ -38,9 +44,11 @@ function execute(actorContext: AnyActorContext, resolvedSendId: string) {
export function cancel<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
TEvent extends EventObject
>(sendId: ResolvableSendId<TContext, TExpressionEvent>) {
function cancel(_: ActionArgs<TContext, TExpressionEvent>) {
TExpressionAction extends ParameterizedObject | undefined
>(sendId: ResolvableSendId<TContext, TExpressionEvent, TExpressionAction>) {
function cancel(
_: ActionArgs<TContext, TExpressionEvent, TExpressionAction>
) {
if (isDevelopment) {
throw new Error(`This isn't supposed to be called`);
}
Expand Down
24 changes: 18 additions & 6 deletions packages/core/src/actions/choose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import {
MachineContext,
AnyActorContext,
AnyState,
ActionArgs
ActionArgs,
ParameterizedObject
} from '../types.ts';
import { evaluateGuard, toGuardDefinition } from '../guards.ts';
import { toArray } from '../utils.ts';

function resolve(
_: AnyActorContext,
state: AnyState,
actionArgs: ActionArgs<any, any>,
actionArgs: ActionArgs<any, any, any>,
{
branches
}: {
Expand All @@ -38,9 +39,17 @@ function resolve(
export function choose<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
TEvent extends EventObject
>(branches: Array<ChooseBranch<TContext, TExpressionEvent>>) {
function choose(_: ActionArgs<TContext, TExpressionEvent>) {
TEvent extends EventObject,
TExpressionAction extends ParameterizedObject | undefined,
TAction extends ParameterizedObject
>(
branches: ReadonlyArray<
ChooseBranch<TContext, TExpressionEvent, TEvent, TAction>
>
) {
function choose(
_: ActionArgs<TContext, TExpressionEvent, TExpressionAction>
) {
if (isDevelopment) {
throw new Error(`This isn't supposed to be called`);
}
Expand All @@ -51,5 +60,8 @@ export function choose<

choose.resolve = resolve;

return choose;
return choose as {
(args: ActionArgs<TContext, TExpressionEvent, TExpressionAction>): void;
_out_TAction?: TAction;
};
}
11 changes: 7 additions & 4 deletions packages/core/src/actions/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
AnyActor,
AnyState,
EventObject,
MachineContext
MachineContext,
ParameterizedObject
} from '../types.ts';
import { resolveReferencedActor } from '../utils.ts';

function resolve(
actorContext: AnyActorContext,
state: AnyState,
actionArgs: ActionArgs<any, any>,
actionArgs: ActionArgs<any, any, any>,
{
id,
systemId,
Expand Down Expand Up @@ -97,7 +98,7 @@ function execute(
export function invoke<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
TEvent extends EventObject
TExpressionAction extends ParameterizedObject | undefined
>({
id,
systemId,
Expand All @@ -109,7 +110,9 @@ export function invoke<
src: string;
input?: unknown;
}) {
function invoke(_: ActionArgs<TContext, TExpressionEvent>) {
function invoke(
_: ActionArgs<TContext, TExpressionEvent, TExpressionAction>
) {
if (isDevelopment) {
throw new Error(`This isn't supposed to be called`);
}
Expand Down
Loading