diff --git a/.changeset/big-pumas-search.md b/.changeset/big-pumas-search.md new file mode 100644 index 0000000000..9a12e6b814 --- /dev/null +++ b/.changeset/big-pumas-search.md @@ -0,0 +1,22 @@ +--- +'xstate': patch +--- + +Make `spawn` input required when defined inside referenced actor: + +```ts +const childMachine = createMachine({ + types: { input: {} as { value: number } } +}); + +const machine = createMachine({ + types: {} as { context: { ref: ActorRefFrom } }, + context: ({ spawn }) => ({ + ref: spawn( + childMachine, + // Input is now required! + { input: { value: 42 } } + ) + }) +}); +``` diff --git a/packages/core/src/spawn.ts b/packages/core/src/spawn.ts index 8ad64f6556..9dcf85ac48 100644 --- a/packages/core/src/spawn.ts +++ b/packages/core/src/spawn.ts @@ -13,7 +13,8 @@ import { IsNotNever, ProvidedActor, RequiredActorOptions, - TODO + TODO, + type RequiredLogicInput } from './types.ts'; import { resolveReferencedActor } from './utils.ts'; @@ -36,33 +37,48 @@ type SpawnOptions< > : never; -export type Spawner = IsLiteralString< - TActor['src'] -> extends true - ? { - ( - logic: TSrc, - ...[options]: SpawnOptions - ): ActorRefFromLogic['logic']>; - ( - src: TLogic, - options?: { - id?: never; - systemId?: string; - input?: InputFrom; - syncSnapshot?: boolean; - } - ): ActorRefFromLogic; - } - : ( - src: TLogic, - options?: { - id?: string; - systemId?: string; - input?: TLogic extends string ? unknown : InputFrom; - syncSnapshot?: boolean; +export type Spawner = + IsLiteralString extends true + ? { + ( + logic: TSrc, + ...[options]: SpawnOptions + ): ActorRefFromLogic['logic']>; + ( + src: TLogic, + ...[options]: ConditionalRequired< + [ + options?: { + id?: never; + systemId?: string; + input?: InputFrom; + syncSnapshot?: boolean; + } & { [K in RequiredLogicInput]: unknown } + ], + IsNotNever> + > + ): ActorRefFromLogic; } - ) => TLogic extends AnyActorLogic ? ActorRefFromLogic : AnyActorRef; + : ( + src: TLogic, + ...[options]: ConditionalRequired< + [ + options?: { + id?: string; + systemId?: string; + input?: TLogic extends string ? unknown : InputFrom; + syncSnapshot?: boolean; + } & (TLogic extends AnyActorLogic + ? { [K in RequiredLogicInput]: unknown } + : {}) + ], + IsNotNever< + TLogic extends AnyActorLogic ? RequiredLogicInput : never + > + > + ) => TLogic extends AnyActorLogic + ? ActorRefFromLogic + : AnyActorRef; export function createSpawner( actorScope: AnyActorScope, @@ -70,8 +86,7 @@ export function createSpawner( event: AnyEventObject, spawnedChildren: Record ): Spawner { - const spawn: Spawner = (src, options = {}) => { - const { systemId, input } = options; + const spawn: Spawner = ((src, options) => { if (typeof src === 'string') { const logic = resolveReferencedActor(machine, src); @@ -82,19 +97,19 @@ export function createSpawner( } const actorRef = createActor(logic, { - id: options.id, + id: options?.id, parent: actorScope.self, - syncSnapshot: options.syncSnapshot, + syncSnapshot: options?.syncSnapshot, input: - typeof input === 'function' - ? input({ + typeof options?.input === 'function' + ? options.input({ context, event, self: actorScope.self }) - : input, + : options?.input, src, - systemId + systemId: options?.systemId }) as any; spawnedChildren[actorRef.id] = actorRef; @@ -102,18 +117,18 @@ export function createSpawner( return actorRef; } else { const actorRef = createActor(src, { - id: options.id, + id: options?.id, parent: actorScope.self, - syncSnapshot: options.syncSnapshot, - input: options.input, + syncSnapshot: options?.syncSnapshot, + input: options?.input, src, - systemId + systemId: options?.systemId }); return actorRef; } - }; - return (src, options) => { + }) as Spawner; + return ((src, options) => { const actorRef = spawn(src, options) as TODO; // TODO: fix types spawnedChildren[actorRef.id] = actorRef; actorScope.defer(() => { @@ -123,5 +138,5 @@ export function createSpawner( actorRef.start(); }); return actorRef; - }; + }) as Spawner; } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1ec3f3a2e0..7a540814aa 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -2,14 +2,14 @@ import type { MachineSnapshot } from './State.ts'; import type { StateMachine } from './StateMachine.ts'; import type { StateNode } from './StateNode.ts'; import { AssignArgs } from './actions/assign.ts'; +import { ExecutableRaiseAction } from './actions/raise.ts'; +import { ExecutableSendToAction } from './actions/send.ts'; import { PromiseActorLogic } from './actors/promise.ts'; -import { Guard, GuardPredicate, UnknownGuard } from './guards.ts'; import type { Actor, ProcessingStatus } from './createActor.ts'; +import { Guard, GuardPredicate, UnknownGuard } from './guards.ts'; +import { InspectionEvent } from './inspection.ts'; import { Spawner } from './spawn.ts'; import { AnyActorSystem, Clock } from './system.js'; -import { InspectionEvent } from './inspection.ts'; -import { ExecutableRaiseAction } from './actions/raise.ts'; -import { ExecutableSendToAction } from './actions/send.ts'; export type Identity = { [K in keyof T]: T[K] }; @@ -805,8 +805,8 @@ export type InvokeConfig< > >; /** - * The transition to take upon the invoked child machine sending an - * error event. + * The transition to take upon the invoked child machine sending an error + * event. */ onError?: | string @@ -2452,6 +2452,9 @@ export type RequiredActorOptions = | (undefined extends TActor['id'] ? never : 'id') | (undefined extends InputFrom ? never : 'input'); +export type RequiredLogicInput = + undefined extends InputFrom ? never : 'input'; + type ExtractLiteralString = T extends string ? string extends T ? never diff --git a/packages/core/test/spawn.test.ts b/packages/core/test/spawn.test.ts new file mode 100644 index 0000000000..af98ed3c70 --- /dev/null +++ b/packages/core/test/spawn.test.ts @@ -0,0 +1,18 @@ +import { ActorRefFrom, createActor, createMachine } from '../src'; + +describe('spawn inside machine', () => { + it('input is required when defined in actor', () => { + const childMachine = createMachine({ + types: { input: {} as { value: number } } + }); + const machine = createMachine({ + types: {} as { context: { ref: ActorRefFrom } }, + context: ({ spawn }) => ({ + ref: spawn(childMachine, { input: { value: 42 }, systemId: 'test' }) + }) + }); + + const actor = createActor(machine).start(); + expect(actor.system.get('test')).toBeDefined(); + }); +}); diff --git a/packages/core/test/spawn.types.test.ts b/packages/core/test/spawn.types.test.ts new file mode 100644 index 0000000000..8f2638cd2a --- /dev/null +++ b/packages/core/test/spawn.types.test.ts @@ -0,0 +1,49 @@ +import { ActorRefFrom, assign, createMachine } from '../src'; + +describe('spawn inside machine', () => { + it('input is required when defined in actor', () => { + const childMachine = createMachine({ + types: { input: {} as { value: number } } + }); + createMachine({ + types: {} as { context: { ref: ActorRefFrom } }, + context: ({ spawn }) => ({ + ref: spawn(childMachine, { input: { value: 42 } }) + }), + initial: 'idle', + states: { + Idle: { + on: { + event: { + actions: assign(({ spawn }) => ({ + ref: spawn(childMachine, { input: { value: 42 } }) + })) + } + } + } + } + }); + }); + + it('input is not required when not defined in actor', () => { + const childMachine = createMachine({}); + createMachine({ + types: {} as { context: { ref: ActorRefFrom } }, + context: ({ spawn }) => ({ + ref: spawn(childMachine) + }), + initial: 'idle', + states: { + Idle: { + on: { + some: { + actions: assign(({ spawn }) => ({ + ref: spawn(childMachine) + })) + } + } + } + } + }); + }); +});