API reference › @evolu/common › Task › RunStateDisposing
Defined in: packages/common/src/Task.ts:976
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<"Disposing">
Properties
type
readonly type: "Disposing";
Defined in: packages/common/src/Type.ts:3492