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.