Newtype Pattern and Semantic Wrappers
Rule
Use newtypes for IDs, units, validated values, and public API meaning; do not wrap primitives when no safety, validation, or clarity is gained.
Why
Newtypes prevent argument swaps, attach validation to values, and give APIs stable domain names independent of primitive representation.
Do
- Keep meaningful or validated inner fields private.
- Use
newfor infallible wrappers andtry_newfor validated values. - Borrow textual input when validation or normalization derives the stored value.
- Expose only focused views and consuming conversions.
- Follow deriving policy for standard traits.
- Follow conversion naming for
as_*andinto_*methods. - Use
#[repr(transparent)]only for a documented layout requirement.
Avoid
- Do not wrap every primitive.
- Do not expose an invariant-bearing inner field publicly.
- Do not implement
Derefmerely to inherit primitive methods. - Do not use
Fromwhen conversion validates or can fail. - Do not choose vague names such as
ValueorIdoutside a narrow domain module.
Public API Notes
Published APIs should use newtypes where primitive arguments can be confused or representation may evolve. Application internals need them mainly at boundaries and for real domain meaning.
Example
#![allow(unused)]
fn main() {
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EmailAddress(String);
impl EmailAddress {
pub fn try_new(value: &str) -> Result<Self, EmailAddressError> {
let value = value.trim();
if !value.contains('@') {
return Err(EmailAddressError::MissingAt);
}
Ok(Self(value.to_owned()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
}
Exceptions
- Use a public tuple field for an intentionally transparent wrapper without invariants or evolution pressure.
- Use
Dereffor a genuinely pointer-like abstraction. - Use a primitive when the value is local, obvious, and does not cross an API boundary.