diff --git a/packages/core/src/setup.ts b/packages/core/src/setup.ts index 0d45d721ef..c214146323 100644 --- a/packages/core/src/setup.ts +++ b/packages/core/src/setup.ts @@ -79,82 +79,78 @@ type ToProvidedActor< type RequiredSetupKeys = IsNever extends true ? never : 'actors'; -export function setup< +type SetupReturn< TContext extends MachineContext, - TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here - TActors extends Record = {}, - TChildrenMap extends Record = {}, - TActions extends Record< - string, - ParameterizedObject['params'] | undefined - > = {}, - TGuards extends Record< - string, - ParameterizedObject['params'] | undefined - > = {}, - TDelay extends string = never, - TTag extends string = string, - TInput = NonReducibleUnknown, - TOutput extends NonReducibleUnknown = NonReducibleUnknown, - TEmitted extends EventObject = EventObject, - TMeta extends MetaObject = MetaObject ->({ - schemas, - actors, - actions, - guards, - delays -}: { - schemas?: unknown; - types?: SetupTypes< + TEvent extends AnyEventObject, + TActors extends Record, + TChildrenMap extends Record, + TActions extends Record, + TGuards extends Record, + TDelay extends string, + TTag extends string, + TInput, + TOutput extends NonReducibleUnknown, + TEmitted extends EventObject, + TMeta extends MetaObject +> = { + extend: < + TExtendActions extends Record< + string, + ParameterizedObject['params'] | undefined + > = {}, + TExtendGuards extends Record< + string, + ParameterizedObject['params'] | undefined + > = {}, + TExtendDelays extends string = never + >({ + actions, + guards, + delays + }: { + actions?: { + [K in keyof TExtendActions]: ActionFunction< + TContext, + TEvent, + TEvent, + TExtendActions[K], + ToProvidedActor, + ToParameterizedObject, + ToParameterizedObject, + TDelay, + TEmitted + >; + }; + guards?: { + [K in keyof TExtendGuards]: GuardPredicate< + TContext, + TEvent, + TExtendGuards[K], + ToParameterizedObject + >; + }; + delays?: { + [K in TExtendDelays]: DelayConfig< + TContext, + TEvent, + ToParameterizedObject['params'], + TEvent + >; + }; + }) => SetupReturn< TContext, TEvent, + TActors, TChildrenMap, + TActions & TExtendActions, + TGuards & TExtendGuards, + TDelay | TExtendDelays, TTag, TInput, TOutput, TEmitted, TMeta >; - actors?: { - // union here enforces that all configured children have to be provided in actors - // it makes those values required here - [K in keyof TActors | Values]: K extends keyof TActors - ? TActors[K] - : never; - }; - actions?: { - [K in keyof TActions]: ActionFunction< - TContext, - TEvent, - TEvent, - TActions[K], - ToProvidedActor, - ToParameterizedObject, - ToParameterizedObject, - TDelay, - TEmitted - >; - }; - guards?: { - [K in keyof TGuards]: GuardPredicate< - TContext, - TEvent, - TGuards[K], - ToParameterizedObject - >; - }; - delays?: { - [K in TDelay]: DelayConfig< - TContext, - TEvent, - ToParameterizedObject['params'], - TEvent - >; - }; -} & { - [K in RequiredSetupKeys]: unknown; -}): { /** * Creates a state config that is strongly typed. This state config can be * used to create a machine. @@ -327,7 +323,97 @@ export function setup< TEvent, ToProvidedActor >; -} { +}; + +export function setup< + TContext extends MachineContext, + TEvent extends AnyEventObject, // TODO: consider using a stricter `EventObject` here + TActors extends Record = {}, + TChildrenMap extends Record = {}, + TActions extends Record< + string, + ParameterizedObject['params'] | undefined + > = {}, + TGuards extends Record< + string, + ParameterizedObject['params'] | undefined + > = {}, + TDelay extends string = never, + TTag extends string = string, + TInput = NonReducibleUnknown, + TOutput extends NonReducibleUnknown = NonReducibleUnknown, + TEmitted extends EventObject = EventObject, + TMeta extends MetaObject = MetaObject +>({ + schemas, + actors, + actions, + guards, + delays +}: { + schemas?: unknown; + types?: SetupTypes< + TContext, + TEvent, + TChildrenMap, + TTag, + TInput, + TOutput, + TEmitted, + TMeta + >; + actors?: { + // union here enforces that all configured children have to be provided in actors + // it makes those values required here + [K in keyof TActors | Values]: K extends keyof TActors + ? TActors[K] + : never; + }; + actions?: { + [K in keyof TActions]: ActionFunction< + TContext, + TEvent, + TEvent, + TActions[K], + ToProvidedActor, + ToParameterizedObject, + ToParameterizedObject, + TDelay, + TEmitted + >; + }; + guards?: { + [K in keyof TGuards]: GuardPredicate< + TContext, + TEvent, + TGuards[K], + ToParameterizedObject + >; + }; + delays?: { + [K in TDelay]: DelayConfig< + TContext, + TEvent, + ToParameterizedObject['params'], + TEvent + >; + }; +} & { + [K in RequiredSetupKeys]: unknown; +}): SetupReturn< + TContext, + TEvent, + TActors, + TChildrenMap, + TActions, + TGuards, + TDelay, + TTag, + TInput, + TOutput, + TEmitted, + TMeta +> { return { assign, sendTo, @@ -349,6 +435,39 @@ export function setup< guards, delays } - ) + ), + extend: (extended) => + setup({ + schemas, + actors, + actions: { ...actions, ...extended.actions }, + guards: { ...guards, ...extended.guards }, + delays: { ...delays, ...extended.delays } + } as any) }; } + +const s = setup({ + actions: { + doSomething: () => {}, + other: () => {} + } +}); + +s.extend({ + actions: { + foo: enqueueActions((x) => { + x.enqueue({ + type: 'doSomething' + }); + }) + } +}).extend({ + actions: { + bar: enqueueActions((x) => { + x.enqueue({ + type: 'foo' + }); + }) + } +}); diff --git a/packages/core/test/setup.types.test.ts b/packages/core/test/setup.types.test.ts index b09c89f44a..eaa6e60f6e 100644 --- a/packages/core/test/setup.types.test.ts +++ b/packages/core/test/setup.types.test.ts @@ -13,6 +13,7 @@ import { fromTransition, log, not, + or, raise, sendParent, sendTo, @@ -2649,6 +2650,133 @@ describe('createStateConfig', () => { }); }); +describe('extend', () => { + it('should allow extending actions and referencing previous ones via enqueueActions across chained extends', () => { + setup({ + actions: { + doSomething: () => {} + } + }) + .extend({ + actions: { + foo: enqueueActions(({ enqueue }) => { + enqueue('doSomething'); + + // @ts-expect-error + enqueue('nonexistent'); + }) + } + }) + .extend({ + actions: { + bar: enqueueActions(({ enqueue }) => { + enqueue('foo'); + + enqueue({ + type: 'doSomething' + }); + + // @ts-expect-error + enqueue('nonexistent'); + }) + } + }) + .createMachine({ + entry: 'bar' + }); + }); + + it('should allow extending guards and referencing previous ones with not/and/or across chained extends', () => { + setup({ + guards: { + truthy: () => true + } + }) + .extend({ + guards: { + alsoTruthy: () => true, + notTruthy: not('truthy'), + // @ts-expect-error + nonexistent: not('existent') + } + }) + .extend({ + guards: { + combined: and(['truthy', 'alsoTruthy']), + alt: or(['notTruthy', 'truthy']), + // @ts-expect-error + nonexistent: or(['existent', 'truthy']) + } + }) + .createMachine({ + on: { + EV: [ + { guard: 'combined', actions: () => {} }, + { guard: 'alt', actions: () => {} }, + { + // @ts-expect-error + guard: 'fake', + actions: () => {} + } + ] + } + }); + }); + + it('should allow extending delays and referencing base delays in actions and transitions', () => { + setup({ + delays: { + short: 10 + } + }) + .extend({ + delays: { + medium: 100 + } + }) + .extend({ + delays: { + long: 1000 + } + }) + .createMachine({ + initial: 'a', + states: { + a: { + entry: [ + raise({ type: 'GO' }, { delay: 'short' }), + raise({ type: 'GO' }, { delay: 'medium' }), + raise({ type: 'GO' }, { delay: 'long' }), + raise( + { type: 'GO' }, + { + // @ts-expect-error + delay: 'nonexistent' + } + ) + ], + on: { + GO: 'b' + } + }, + b: { + after: { + medium: 'c' + } + }, + c: { + after: { + long: 'd', + // @ts-expect-error + nonexistent: 'd' + } + }, + d: {} + } + }); + }); +}); + describe('type-bound actions', () => { it('should be able to create a type-safe action action', () => { const machineSetup = setup({