diff --git a/.changeset/stale-plants-design.md b/.changeset/stale-plants-design.md new file mode 100644 index 0000000000..ecb172b8f3 --- /dev/null +++ b/.changeset/stale-plants-design.md @@ -0,0 +1,5 @@ +--- +'xstate': patch +--- + +Fixed event type narrowing in some of the builtin actions. diff --git a/packages/core/src/actions.ts b/packages/core/src/actions.ts index d4be1c577a..07a289a887 100644 --- a/packages/core/src/actions.ts +++ b/packages/core/src/actions.ts @@ -398,10 +398,14 @@ const defaultLogExpr = ( * - `event` - the event that caused this action to be executed. * @param label The label to give to the logged expression. */ -export function log( - expr: string | LogExpr = defaultLogExpr, +export function log< + TContext, + TExpressionEvent extends EventObject, + TEvent extends EventObject = TExpressionEvent +>( + expr: string | LogExpr = defaultLogExpr, label?: string -): LogAction { +): LogAction { return { type: actionTypes.log, label, @@ -431,9 +435,13 @@ export const resolveLog = ( * * @param sendId The `id` of the `send(...)` action to cancel. */ -export const cancel = ( +export const cancel = < + TContext, + TExpressionEvent extends EventObject, + TEvent extends EventObject +>( sendId: string | number -): CancelAction => { +): CancelAction => { return { type: actionTypes.cancel, sendId @@ -462,9 +470,13 @@ export function start( * * @param actorRef The activity to stop. */ -export function stop( - actorRef: string | Expr -): StopAction { +export function stop< + TContext, + TExpressionEvent extends EventObject, + TEvent extends EventObject = TExpressionEvent +>( + actorRef: string | Expr +): StopAction { const activity = isFunction(actorRef) ? actorRef : toActivityDefinition(actorRef); @@ -584,18 +596,22 @@ export function error(id: string, data?: any): ErrorPlatformEvent & string { return eventObject as ErrorPlatformEvent & string; } -export function pure( +export function pure< + TContext, + TExpressionEvent extends EventObject, + TEvent extends EventObject = TExpressionEvent +>( getActions: ( context: TContext, - event: TEvent + event: TExpressionEvent ) => | SingleOrArray< | BaseActionObject | BaseActionObject['type'] - | ActionObject + | ActionObject > | undefined -): PureAction { +): PureAction { return { type: ActionTypes.Pure, get: getActions @@ -664,9 +680,13 @@ export function escalate< ); } -export function choose( - conds: Array> -): ChooseAction { +export function choose< + TContext, + TExpressionEvent extends EventObject, + TEvent extends EventObject = TExpressionEvent +>( + conds: Array> +): ChooseAction { return { type: ActionTypes.Choose, conds diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index bca30415eb..14cf844585 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -127,9 +127,13 @@ export type ActionFunction< ): void; }['bivarianceHack']; -export interface ChooseCondition { - cond?: Condition; - actions: Actions; +export interface ChooseCondition< + TContext, + TExpressionEvent extends EventObject, + TEvent extends EventObject = TExpressionEvent +> { + cond?: Condition; + actions: Actions; } export type Action< @@ -155,9 +159,8 @@ type SimpleActionsOf = ActionObject< /** * Events that do not require payload */ -export type SimpleEventsOf< - TEvent extends EventObject -> = ExtractWithSimpleSupport; +export type SimpleEventsOf = + ExtractWithSimpleSupport; export type BaseAction< TContext, @@ -1985,16 +1988,15 @@ type Matches = { (stateValue: TypegenDisabledArg): any; }; -export type StateValueFrom< - TMachine extends AnyStateMachine -> = StateFrom['matches'] extends Matches< - infer TypegenEnabledArg, - infer TypegenDisabledArg -> - ? TMachine['__TResolvedTypesMeta'] extends TypegenEnabled - ? TypegenEnabledArg - : TypegenDisabledArg - : never; +export type StateValueFrom = + StateFrom['matches'] extends Matches< + infer TypegenEnabledArg, + infer TypegenDisabledArg + > + ? TMachine['__TResolvedTypesMeta'] extends TypegenEnabled + ? TypegenEnabledArg + : TypegenDisabledArg + : never; export type PredictableActionArgumentsExec = ( action: ActionObject, diff --git a/packages/core/test/actionCreators.test.ts b/packages/core/test/actionCreators.test.ts index cfac0a6473..370db63bfb 100644 --- a/packages/core/test/actionCreators.test.ts +++ b/packages/core/test/actionCreators.test.ts @@ -7,7 +7,7 @@ describe('action creators', () => { (['start', 'stop'] as const).forEach((actionKey) => { describe(`${actionKey}()`, () => { it('should accept a string action', () => { - const action = actions[actionKey]('test'); + const action = (actions[actionKey] as any)('test'); expect(action.type).toEqual(actionTypes[actionKey]); expect(action).toEqual({ type: actionTypes[actionKey], @@ -21,7 +21,10 @@ describe('action creators', () => { }); it('should accept an action object', () => { - const action = actions[actionKey]({ type: 'test', foo: 'bar' } as any); + const action = (actions[actionKey] as any)({ + type: 'test', + foo: 'bar' + } as any); expect(action.type).toEqual(actionTypes[actionKey]); expect(action).toEqual({ type: actionTypes[actionKey], @@ -35,7 +38,7 @@ describe('action creators', () => { }); it('should accept an activity definition', () => { - const action = actions[actionKey]({ + const action = (actions[actionKey] as any)({ type: 'test', foo: 'bar', src: 'someSrc' diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index ea58bec879..6cb04e6aaf 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -8,7 +8,7 @@ import { spawn, ActorRefFrom } from '../src/index'; -import { raise, stop } from '../src/actions'; +import { raise, stop, log } from '../src/actions'; import { createModel } from '../src/model'; function noop(_x: unknown) { @@ -319,6 +319,46 @@ describe('Raise events', () => { }); }); +describe('log', () => { + it('should narrow down the event type in the expression', () => { + createMachine({ + schema: { + events: {} as { type: 'FOO' } | { type: 'BAR' } + }, + on: { + FOO: { + actions: log((_ctx, ev) => { + ((_arg: 'FOO') => {})(ev.type); + // @ts-expect-error + ((_arg: 'BAR') => {})(ev.type); + }) + } + } + }); + }); +}); + +describe('stop', () => { + it('should narrow down the event type in the expression', () => { + createMachine({ + schema: { + events: {} as { type: 'FOO' } | { type: 'BAR' } + }, + on: { + FOO: { + actions: stop((_ctx, ev) => { + ((_arg: 'FOO') => {})(ev.type); + // @ts-expect-error + ((_arg: 'BAR') => {})(ev.type); + + return 'fakeId'; + }) + } + } + }); + }); +}); + describe('Typestates', () => { // Using "none" because undefined and null are unavailable when not in strict mode. type None = { type: 'none' };