When one type can stand in for another
A value can be used where its own type is expected, and where a type it
widens to is expected. Widening means stepping from a type to the more
general one it was declared from. Going the other way always goes through a
constructor. With Customer and Supplier entities that each have an
email: Email field:
| You have | Where Customer.email is expected |
Where Email is expected |
Where Text is expected |
|---|---|---|---|
Customer.email |
yes | yes, it widens | no |
Supplier.email |
no | yes, it widens | no |
Email |
only through Customer.email(...) |
yes | no |
Text |
only through Customer.email(...) |
only through Email(...) |
yes |
- Widening follows declared steps only: a field type to its declared type, a named type to the named type it builds on.
- No silent narrowing. Going from a general type to a specific one always goes through a constructor, which re-checks the rules.
- Same shape is not same type. Two objects with identical fields are still different types.
- Nothing unwraps. A named type never becomes its built-in representation, except where arithmetic and comparison operators work on it.
Customer.id and Supplier.id stay distinct even when both are version 4
UUIDs. A function that genuinely accepts either takes an enum whose
variants carry each one, which keeps the choice explicit. Your own code has
no generics or unions (Generic code).
Markdown for agents: /docs/when-one-type-can-stand-in-for-another.md.Built from Jadpo main at a707efb.