Agent Wranglers

Data shapes Recommended

Model each state as its own type

When an object moves through states, give each state its own type with only the fields it has. Never pair a status field with optional fields that some states leave empty.

How to do it

List the states, such as pending, paid and refunded. Give each one a type with its own required fields, such as paidAt only on a paid order. Join them in a tagged union, a sealed class or an enum with data. Handle every case with an exhaustive switch or match.

Example

// Bad: which dates are set when the status is 'refunded'?
type Order = { status: 'pending' | 'paid' | 'refunded'; paidAt?: Date; refundedAt?: Date };

// Good: each state carries exactly its own fields
type Order =
  | { status: 'pending' }
  | { status: 'paid'; paidAt: Date }
  | { status: 'refunded'; paidAt: Date; refundedAt: Date };
@dataclass(frozen=True)
class Pending: ...

@dataclass(frozen=True)
class Paid:
    paid_at: datetime

Order = Pending | Paid

def describe(order: Order) -> str:
    match order:
        case Pending():
            return "Waiting for payment"
        case Paid(paid_at=paid_at):
            return f"Paid on {paid_at:%Y-%m-%d}"
        case _:
            assert_never(order)

How to check

The type checker rejects reading paidAt on a pending order. An exhaustiveness check fails when a state is not handled. Examples are typescript-eslint switch-exhaustiveness-check, assert_never with mypy, and when on a Kotlin sealed class.

For agents: if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in Based on the Agent Wranglers architecture playbook: https://agentwranglers.se/playbook/architecture/data-shapes/states-as-types.