API reference › @evolu/common › Task › 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
Typed<"Settled">
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