Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions packages/core/src/actors/promise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
Snapshot
} from '../types.ts';

export type PromiseSnapshot<TOutput, TInput> = Snapshot<TOutput> & {
export type PromiseSnapshot<TOutput, TInput, TError = unknown> = Snapshot<
TOutput,
TError
> & {
input: TInput | undefined;
};

Expand All @@ -19,9 +22,10 @@ const XSTATE_PROMISE_REJECT = 'xstate.promise.reject';
export type PromiseActorLogic<
TOutput,
TInput = unknown,
TEmitted extends EventObject = EventObject
TEmitted extends EventObject = EventObject,
TError = unknown
> = ActorLogic<
PromiseSnapshot<TOutput, TInput>,
PromiseSnapshot<TOutput, TInput, TError>,
{ type: string; [k: string]: unknown },
TInput, // input
AnyActorSystem,
Expand Down Expand Up @@ -61,8 +65,8 @@ export type PromiseActorLogic<
*
* @see {@link fromPromise}
*/
export type PromiseActorRef<TOutput> = ActorRefFromLogic<
PromiseActorLogic<TOutput, unknown>
export type PromiseActorRef<TOutput, TError = unknown> = ActorRefFromLogic<
PromiseActorLogic<TOutput, unknown, EventObject, TError>
>;

const controllerMap = new WeakMap<AnyActorRef, AbortController>();
Expand Down Expand Up @@ -120,7 +124,8 @@ const controllerMap = new WeakMap<AnyActorRef, AbortController>();
export function fromPromise<
TOutput,
TInput = NonReducibleUnknown,
TEmitted extends EventObject = EventObject
TEmitted extends EventObject = EventObject,
TError = unknown
Copy link
Collaborator

@Andarist Andarist Jul 8, 2025

Choose a reason for hiding this comment

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

As much as I'd like to have typed errors - I don't think this is good. We absolutely can't guarantee the correctness of this type parameter. This is as good as a type cast - but it's way more dangerous because the user won't immediately realize this. In other words, it can give somebody a false sense of safety.

Copy link
Author

Choose a reason for hiding this comment

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

This isn’t something I expect most people to use, but as far as I know, there’s no other way to implement something like the neverthrow wrapper I created without being able to pass the error type. Ideally, this functionality would be hidden from most users while still being accessible for those who truly need it and are comfortable type casting the error type in cases where it’s known (e.g., neverthrow, effect-ts, etc.)

If you have any suggestions for alternative solutions, I'd love to hear them.

An alternative idea would be to omit the error type additions in the fromPromise function while keeping the updates in the types file. This would allow custom variants of PromiseActorLogic and PromiseSnapshot with the error type to remain compatible with the rest of xstate. However, this approach would still require adding the ErrorFrom type.

Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. ErrorFrom can't simply use the TError type. We could add that generic but the final error type would always have to be a union of that and unknown (which is tricky because T | unknown is just unknown but perhaps we could utilize our NonReducibleUnknown for this). It's the only way to make this safe
  2. Even with neverthrow you don't end up trusting the generic itself, you actually have to adapt the unknown error into a known error using a function. I'd much more prefer a design closer to this.
  3. The PR shouldn't focus on promises alone - we'd have to figure out how to make the new additon play well with other actor types and add appropriate type-level tests

>(
promiseCreator: ({
input,
Expand All @@ -138,8 +143,8 @@ export function fromPromise<
signal: AbortSignal;
emit: (emitted: TEmitted) => void;
}) => PromiseLike<TOutput>
): PromiseActorLogic<TOutput, TInput, TEmitted> {
const logic: PromiseActorLogic<TOutput, TInput, TEmitted> = {
): PromiseActorLogic<TOutput, TInput, TEmitted, TError> {
const logic: PromiseActorLogic<TOutput, TInput, TEmitted, TError> = {
config: promiseCreator,
transition: (state, event, scope) => {
if (state.status !== 'active') {
Expand Down
25 changes: 19 additions & 6 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,19 @@ export type OutputFrom<T> =
? (TSnapshot & { status: 'done' })['output']
: never;

export type ErrorFrom<T> =
T extends ActorLogic<
infer TSnapshot,
infer _TEvent,
infer _TInput,
infer _TSystem,
infer _TEmitted
>
? (TSnapshot & { status: 'error' })['error']
: T extends ActorRef<infer TSnapshot, infer _TEvent, infer _TEmitted>
? (TSnapshot & { status: 'error' })['error']
: never;

export type ActionFunction<
TContext extends MachineContext,
TExpressionEvent extends EventObject,
Expand Down Expand Up @@ -420,7 +433,7 @@ export interface InvokeDefinition<
| SingleOrArray<
TransitionConfig<
TContext,
ErrorActorEvent,
ErrorActorEvent<unknown>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -670,7 +683,7 @@ type DistributeActors<
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
ErrorActorEvent,
ErrorActorEvent<ErrorFrom<TSpecificActor['logic']>>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -725,7 +738,7 @@ type DistributeActors<
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
ErrorActorEvent,
ErrorActorEvent<unknown>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -818,7 +831,7 @@ export type InvokeConfig<
| SingleOrArray<
TransitionConfigOrTarget<
TContext,
ErrorActorEvent,
ErrorActorEvent<unknown>,
TEvent,
TActor,
TAction,
Expand Down Expand Up @@ -2182,7 +2195,7 @@ export type AnyActorScope = ActorScope<

export type SnapshotStatus = 'active' | 'done' | 'error' | 'stopped';

export type Snapshot<TOutput> =
export type Snapshot<TOutput, TError = unknown> =
| {
status: 'active';
output: undefined;
Expand All @@ -2196,7 +2209,7 @@ export type Snapshot<TOutput> =
| {
status: 'error';
output: undefined;
error: unknown;
error: TError;
}
| {
status: 'stopped';
Expand Down