API reference@evolu/commonTask › RunStateSettled

Defined in: packages/common/src/Task.ts:978

Base interface for objects with a discriminant type property.

This enables discriminated unions (also known as tagged unions) — a pattern where TypeScript uses a literal type field to narrow union types automatically.

Why Discriminated Unions?

Discriminated unions model states that are mutually exclusive. Instead of optional fields and boolean flags that can combine into invalid configurations, each variant is a distinct type. This makes illegal states unrepresentable — invalid combinations cannot exist, so bugs cannot create them.

Benefits:

  • Self-documenting — Union cases immediately show all possible states
  • Compile-time safety — TypeScript enforces handling all cases
  • Refactoring-friendly — Adding a new state breaks code that doesn't handle it

Example

// Bad: optional fields allow invalid states (no contact info at all)
interface Contact {
  readonly email?: Email;
  readonly phone?: Phone;
}

// Good: discriminated union makes "at least one" explicit
interface EmailOnly extends Typed<"EmailOnly"> {
  readonly email: Email;
}
interface PhoneOnly extends Typed<"PhoneOnly"> {
  readonly phone: Phone;
}
interface EmailAndPhone extends Typed<"EmailAndPhone"> {
  readonly email: Email;
  readonly phone: Phone;
}

type ContactInfo = EmailOnly | PhoneOnly | EmailAndPhone;
interface Pending extends Typed<"Pending"> {
  readonly createdAt: DateIso;
}
interface Shipped extends Typed<"Shipped"> {
  readonly trackingNumber: TrackingNumber;
}
interface Delivered extends Typed<"Delivered"> {
  readonly deliveredAt: DateIso;
}
interface Cancelled extends Typed<"Cancelled"> {
  readonly reason: CancellationReason;
}

type OrderState = Pending | Shipped | Delivered | Cancelled;

// TypeScript enforces exhaustiveness via return type
const getStatusMessage = (state: OrderState): string => {
  switch (state.type) {
    case "Pending":
      return "Order placed";
    case "Shipped":
      return `Shipped: ${state.trackingNumber}`;
    case "Delivered":
      return `Delivered on ${state.deliveredAt.toLocaleDateString()}`;
    case "Cancelled":
      return `Cancelled: ${state.reason}`;
  }
};

// For void functions, use exhaustiveCheck to ensure all cases are handled
const logState = (state: OrderState): void => {
  switch (state.type) {
    case "Pending":
      console.log("Order placed");
      break;
    case "Shipped":
      console.log(`Shipped: ${state.trackingNumber}`);
      break;
    case "Delivered":
      console.log(`Delivered on ${state.deliveredAt.toLocaleDateString()}`);
      break;
    case "Cancelled":
      console.log(`Cancelled: ${state.reason}`);
      break;
    default:
      exhaustiveCheck(state);
  }
};

Why type (and not e.g. _tag)?

Underscore-prefixing is meant to avoid clashing with domain properties, but proper discriminated union design means the discriminant IS the domain concept — there's no clash to avoid. The type prop name also aligns with Type's name. If an entity has a meaningful "type" (like product category), model it as the discriminant itself:

interface Electronics extends Typed<"Electronics"> {
  voltage: Voltage;
}
interface Clothing extends Typed<"Clothing"> {
  size: Size;
}
type Product = Electronics | Clothing;

See

  • exhaustiveCheck to ensure all cases are handled in void functions.
  • typed for runtime-validated typed objects.

Extends

Properties

outcome

readonly outcome: Result<T, E>;

Defined in: packages/common/src/Task.ts:996

What the Task actually returned.

Unlike result, not overridden by abort.


result

readonly result: Result<T, E>;

Defined in: packages/common/src/Task.ts:989

The Run's completion value.

If abort was requested, this is AbortError even if the Task completed successfully — see outcome for what the Task actually returned.


type

readonly type: "Settled";

Defined in: packages/common/src/Type.ts:3492

Inherited from

Typed.type