Skip to content

Commit

Permalink
Input types for machines (#4054)
Browse files Browse the repository at this point in the history
* Typed input

* Cleaning up some types

* Add AnythingButAFunction to clean up types

* Fix tests

* fix type issues

* fixed tests

* tweak tests

* Remove `AnythingButAFunction`

* Remove incorrect forwarding of the `TInput`

* Introduce `NonReducibleUnknown`

* tweak actor types

* Add small test for input mapper

* Add test for output

* Add self to property mapper

* Remove property mapper type for output

* Simplify mapContext function and add dev-time warning

* Add changeset

* handle my own nits

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist committed Aug 1, 2023
1 parent ead1e43 commit a247111
Show file tree
Hide file tree
Showing 34 changed files with 467 additions and 224 deletions.
29 changes: 29 additions & 0 deletions .changeset/odd-readers-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'xstate': minor
---

Input types can now be specified for machines:

```ts
const emailMachine = createMachine({
types: {} as {
input: {
subject: string;
message: string;
};
},
context: ({ input }) => ({
// Strongly-typed input!
emailSubject: input.subject,
emailBody: input.message.trim()
})
});

const emailActor = interpret(emailMachine, {
input: {
// Strongly-typed input!
subject: 'Hello, world!',
message: 'This is a test.'
}
}).start();
```
9 changes: 6 additions & 3 deletions packages/core/src/Machine.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
MachineConfig,
EventObject,
AnyEventObject,
MachineContext,
InternalMachineImplementations,
ParameterizedObject,
ProvidedActor
ProvidedActor,
AnyEventObject
} from './types.ts';
import {
TypegenConstraint,
Expand All @@ -18,13 +18,15 @@ export function createMachine<
TContext extends MachineContext,
TEvent extends EventObject = AnyEventObject,
TActor extends ProvidedActor = ProvidedActor,
TInput = any,
TTypesMeta extends TypegenConstraint = TypegenDisabled
>(
config: MachineConfig<
TContext,
TEvent,
ParameterizedObject,
TActor,
TInput,
TTypesMeta
>,
implementations?: InternalMachineImplementations<
Expand All @@ -39,9 +41,10 @@ export function createMachine<
TEvent,
ParameterizedObject,
TActor,
TInput,
ResolveTypegenMeta<TTypesMeta, TEvent, ParameterizedObject, TActor>
> {
return new StateMachine<any, any, any, any, any>(
return new StateMachine<any, any, any, any, any, any>(
config as any,
implementations as any
);
Expand Down
26 changes: 18 additions & 8 deletions packages/core/src/StateMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ import type {
AnyEventObject,
ProvidedActor,
AnyActorRef,
Equals
Equals,
TODO
} from './types.ts';
import { isErrorEvent, resolveReferencedActor } from './utils.ts';

Expand All @@ -52,9 +53,10 @@ export const WILDCARD = '*';

export class StateMachine<
TContext extends MachineContext,
TEvent extends EventObject = EventObject,
TAction extends ParameterizedObject = ParameterizedObject,
TActor extends ProvidedActor = ProvidedActor,
TEvent extends EventObject,
TAction extends ParameterizedObject,
TActor extends ProvidedActor,
TInput,
TResolvedTypesMeta = ResolveTypegenMeta<
TypegenDisabled,
NoInfer<TEvent>,
Expand All @@ -66,7 +68,12 @@ export class StateMachine<
TEvent,
State<TContext, TEvent, TActor, TResolvedTypesMeta>,
State<TContext, TEvent, TActor, TResolvedTypesMeta>,
PersistedMachineState<State<TContext, TEvent, TActor, TResolvedTypesMeta>>
PersistedMachineState<
State<TContext, TEvent, TActor, TResolvedTypesMeta>
>,
TODO,
TInput,
TODO
>
{
/**
Expand All @@ -76,7 +83,7 @@ export class StateMachine<

public implementations: MachineImplementationsSimplified<TContext, TEvent>;

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

public __xstatenode: true = true;

Expand Down Expand Up @@ -109,7 +116,7 @@ export class StateMachine<

this.root = new StateNode(config, {
_key: this.id,
_machine: this
_machine: this as any
});

this.root._initialize();
Expand Down Expand Up @@ -141,6 +148,7 @@ export class StateMachine<
TEvent,
TAction,
TActor,
TInput,
AreAllImplementationsAssumedToBeProvided<TResolvedTypesMeta> extends false
? MarkAllImplementationsAsProvided<TResolvedTypesMeta>
: TResolvedTypesMeta
Expand Down Expand Up @@ -278,7 +286,7 @@ export class StateMachine<
TEvent,
State<TContext, TEvent, TActor, TResolvedTypesMeta>
>,
input?: any
input?: TInput
): State<TContext, TEvent, TActor, TResolvedTypesMeta> {
const initEvent = createInitEvent(input) as unknown as TEvent; // TODO: fix;

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

__TInput!: TInput;
}
7 changes: 3 additions & 4 deletions packages/core/src/StateNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,16 @@ export class StateNode<
/**
* The root machine node.
*/
public machine: StateMachine<TContext, TEvent, any, any>;
public machine: StateMachine<TContext, TEvent, any, any, TODO, TODO>;
/**
* The meta data associated with this state node, which will be returned in State instances.
*/
public meta?: any;
/**
* The output data sent with the "done.state._id_" event if this is a final state node.
*/
public output?:
| Mapper<TContext, TEvent, any>
| PropertyMapper<TContext, TEvent, any>;
public output?: Mapper<TContext, TEvent, any>;

/**
* The order this state node appears. Corresponds to the implicit document order.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,6 @@ export function error(id: string, data?: any): ErrorPlatformEvent & string {
return eventObject as ErrorPlatformEvent & string;
}

export function createInitEvent(input: any) {
export function createInitEvent(input: unknown) {
return { type: INIT_TYPE, input } as const;
}
4 changes: 2 additions & 2 deletions packages/core/src/actions/invoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function resolve(
id: string;
systemId: string | undefined;
src: string;
input?: any;
input?: unknown;
}
) {
const referenced = resolveReferencedActor(
Expand Down Expand Up @@ -107,7 +107,7 @@ export function invoke<
id: string;
systemId: string | undefined;
src: string;
input?: any;
input?: unknown;
}) {
function invoke(_: ActionArgs<TContext, TExpressionEvent>) {
if (isDevelopment) {
Expand Down
26 changes: 14 additions & 12 deletions packages/core/src/actors/callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,33 @@ import { isPromiseLike } from '../utils';
import { doneInvoke, error } from '../actions.ts';
import { startSignalType, stopSignalType, isSignal } from '../actors/index.ts';

export interface CallbackInternalState<TEvent extends EventObject> {
export interface CallbackInternalState<
TEvent extends EventObject,
TInput = unknown
> {
canceled: boolean;
receivers: Set<(e: TEvent) => void>;
dispose: void | (() => void) | Promise<any>;
input?: any;
input: TInput;
}

export type CallbackActorLogic<
TEvent extends EventObject,
TInput = any
TInput = unknown
> = ActorLogic<
TEvent,
undefined,
CallbackInternalState<TEvent>,
CallbackInternalState<TEvent>,
CallbackInternalState<TEvent, TInput>,
Pick<CallbackInternalState<TEvent, TInput>, 'input' | 'canceled'>,
ActorSystem<any>,
TInput,
any
>;

export type CallbackActorRef<TEvent extends EventObject> = ActorRefFrom<
CallbackActorLogic<TEvent>
>;
export type CallbackActorRef<
TEvent extends EventObject,
TInput = unknown
> = ActorRefFrom<CallbackActorLogic<TEvent, TInput>>;

export type Receiver<TEvent extends EventObject> = (
listener: {
Expand Down Expand Up @@ -62,7 +66,7 @@ export type InvokeCallback<
export function fromCallback<TEvent extends EventObject, TInput>(
invokeCallback: InvokeCallback<TEvent, AnyEventObject, TInput>
): CallbackActorLogic<TEvent, TInput> {
const logic: CallbackActorLogic<TEvent, TInput> = {
return {
config: invokeCallback,
start: (_state, { self }) => {
self.send({ type: startSignalType } as TEvent);
Expand Down Expand Up @@ -131,8 +135,6 @@ export function fromCallback<TEvent extends EventObject, TInput>(
};
},
getSnapshot: () => undefined,
getPersistedState: ({ input }) => input
getPersistedState: ({ input, canceled }) => ({ input, canceled })
};

return logic;
}
43 changes: 14 additions & 29 deletions packages/core/src/actors/observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,23 @@ import {
} from '../types';
import { stopSignalType } from '../actors';

export interface ObservableInternalState<T> {
export interface ObservableInternalState<T, TInput = unknown> {
subscription: Subscription | undefined;
status: 'active' | 'done' | 'error' | 'canceled';
data: T | undefined;
input?: any;
input: TInput | undefined;
}

export type ObservablePersistedState<T> = Omit<
ObservableInternalState<T>,
export type ObservablePersistedState<T, TInput = unknown> = Omit<
ObservableInternalState<T, TInput>,
'subscription'
>;

export type ObservableActorLogic<T, TInput> = ActorLogic<
EventObject,
{ type: string; [k: string]: unknown },
T | undefined,
ObservableInternalState<T>,
ObservablePersistedState<T>,
ObservableInternalState<T, TInput>,
ObservablePersistedState<T, TInput>,
AnyActorSystem,
TInput
>;
Expand All @@ -45,13 +45,7 @@ export function fromObservable<T, TInput>(
const errorEventType = '$$xstate.error';
const completeEventType = '$$xstate.complete';

// TODO: add event types
const logic: ActorLogic<
any,
T | undefined,
ObservableInternalState<T>,
ObservablePersistedState<T>
> = {
return {
config: observableCreator,
transition: (state, event, { self, id, defer }) => {
if (state.status !== 'active') {
Expand All @@ -70,14 +64,14 @@ export function fromObservable<T, TInput>(
});
return {
...state,
data: event.data
data: (event as any).data
};
case errorEventType:
return {
...state,
status: 'error',
input: undefined,
data: event.data,
data: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
subscription: undefined
};
case completeEventType:
Expand Down Expand Up @@ -113,7 +107,7 @@ export function fromObservable<T, TInput>(
return;
}
state.subscription = observableCreator({
input: state.input,
input: state.input!,
system,
self
}).subscribe({
Expand All @@ -140,8 +134,6 @@ export function fromObservable<T, TInput>(
subscription: undefined
})
};

return logic;
}

/**
Expand All @@ -167,12 +159,7 @@ export function fromEventObservable<T extends EventObject, TInput>(
const completeEventType = '$$xstate.complete';

// TODO: event types
const logic: ActorLogic<
any,
T | undefined,
ObservableInternalState<T>,
ObservablePersistedState<T>
> = {
return {
config: lazyObservable,
transition: (state, event) => {
if (state.status !== 'active') {
Expand All @@ -185,7 +172,7 @@ export function fromEventObservable<T extends EventObject, TInput>(
...state,
status: 'error',
input: undefined,
data: event.data,
data: (event as any).data, // TODO: if we keep this as `data` we should reflect this in the type
subscription: undefined
};
case completeEventType:
Expand Down Expand Up @@ -222,7 +209,7 @@ export function fromEventObservable<T extends EventObject, TInput>(
}

state.subscription = lazyObservable({
input: state.input,
input: state.input!,
system,
self
}).subscribe({
Expand All @@ -249,6 +236,4 @@ export function fromEventObservable<T extends EventObject, TInput>(
subscription: undefined
})
};

return logic;
}
Loading

0 comments on commit a247111

Please sign in to comment.