Das Newtype-Pattern ist eines der einflussreichsten und am häufigsten verwendeten Patterns in Rust. Es besteht aus einem Tuple-Struct mit genau einem Feld — eine extrem einfache Konstruktion, die drei mächtige Probleme löst: Typsicherheit (ein UserId ist nicht das gleiche wie ein OrderId, auch wenn beide intern u64 sind), Trait-Implementierungen über Stdlib-Typen hinweg (du darfst Display für deinen eigenen MyVec(Vec<T>) schreiben — für Vec<T> selbst nicht), und Domain-Validierung (Email-Wrapper, der nur valide Emails enthalten kann). Dieser Artikel zeigt jedes dieser Patterns mit konkreten Beispielen.
Die Form
struct Meter(f64);
struct UserId(u64);
struct ApiToken(String);
struct EinmalRequest(Request);
# struct Request;Ein Newtype ist ein Tuple-Struct mit genau einem Feld. Auf der Maschinen-Ebene wirkt er wie der innere Typ — kein Speicher-Overhead, keine Performance-Kosten. Im Typ-System ist er aber ein eigener Typ, was alle Verwechslungs-Bugs zur Compile-Zeit fängt.
Anwendung 1: Typsicherheit
Das häufigste Anwendungs-Gebiet. Ohne Newtype:
fn ueberweisen(von_user: u64, an_user: u64, betrag_cent: u64) {
// Was, wenn der Aufrufer die Reihenfolge verwechselt?
// ueberweisen(betrag, von, an); // alles u64, Compiler merkt nichts
println!("Übertrage {betrag_cent} Cent von User {von_user} an User {an_user}");
}Mit Newtype:
#[derive(Debug, Clone, Copy)]
struct UserId(u64);
#[derive(Debug, Clone, Copy)]
struct Cents(u64);
fn ueberweisen(von: UserId, an: UserId, betrag: Cents) {
println!("Übertrage {} Cent von User {} an User {}", betrag.0, von.0, an.0);
}
fn main() {
let alice = UserId(42);
let bert = UserId(100);
let betrag = Cents(2500);
ueberweisen(alice, bert, betrag);
// ueberweisen(betrag, alice, bert); // Compile-Fehler — Typen vertauscht
}Reihenfolge-Verwechslungen werden vom Compiler erkannt. Erfundene Identifier-Typen sind eines der einfachsten Werkzeuge, um eine ganze Klasse von Bugs zu vermeiden.
Anwendung 2: Trait-Impls trotz Orphan-Rule
Die Orphan-Rule in Rust besagt: du darfst einen Trait T für einen Typ U nur dann implementieren, wenn entweder T oder U aus deinem Crate stammt. Sonst Compile-Fehler.
Konkret: wenn du Display (Stdlib-Trait) für Vec<T> (Stdlib-Typ) implementieren willst, geht das nicht — beide sind extern für dich.
Lösung: Newtype!
use std::fmt;
// Ein eigener Wrapper — eigener Typ aus deinem Crate
struct MeineVec(Vec<i32>);
impl fmt::Display for MeineVec {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "MeineVec({} Elemente)", self.0.len())
}
}
fn main() {
let v = MeineVec(vec![1, 2, 3]);
println!("{v}"); // "MeineVec(3 Elemente)"
}Über den Wrapper darfst du beliebige Traits implementieren. Sehr klassische Anwendung für domain-spezifische Custom-Display-Formatierungen.
Anwendung 3: Validierung am Konstruktor
Ein Newtype mit privatem Feld plus Konstruktor-Funktion garantiert, dass nur valide Werte konstruierbar sind:
pub struct Email(String);
impl Email {
pub fn neu(s: String) -> Result<Self, &'static str> {
if !s.contains('@') || !s.contains('.') {
return Err("ungültige Email-Adresse");
}
Ok(Email(s))
}
pub fn als_str(&self) -> &str { &self.0 }
}
fn main() {
let valid = Email::neu("user@example.com".into()).unwrap();
let invalid = Email::neu("nicht-valid".into());
assert!(invalid.is_err());
// Email kann nie ungültig sein — Validierung beim Konstruktor garantiert das.
println!("{}", valid.als_str());
}Das Feld ist privat. Wer außerhalb des Moduls eine Email erstellt, muss durch Email::neu(...) — und der prüft. Im Code, der Email-Werte annimmt, gilt: wenn es eine Email ist, ist sie valide. Keine zusätzlichen Checks nötig.
Klassisches Parse, Don't Validate-Pattern: einmal beim Konstruktor validieren, danach im Typ-System darauf verlassen.
repr(transparent) für garantierten Zero-Cost
Wenn du sicherstellen willst, dass dein Newtype im Speicher identisch zum inneren Typ ist (wichtig bei FFI), nutzt du #[repr(transparent)]:
#[repr(transparent)]
pub struct Meter(pub f64);
fn main() {
use std::mem::size_of;
assert_eq!(size_of::<Meter>(), size_of::<f64>()); // 8
// Garantiert keine Padding, kein Header — Meter im Speicher == f64.
}Ohne #[repr(transparent)] ist das auch fast immer der Fall — aber nicht garantiert vom Compiler. #[repr(transparent)] macht es zur API-Garantie, wichtig bei:
- FFI mit C — Funktionen, die
Meter-Pointer übergeben, sind binär-kompatibel zuf64-Pointern. - Stable ABI über Bibliotheks-Versionen.
- Unsafe-Casting zwischen Wrapper und Innerem (nur in
unsafe-Blöcken legitim).
Newtype + From / Into für bequeme Konvertierung
Wer einen Newtype hat, ergänzt häufig From- und Into-Impls für transparente Konvertierung:
pub struct UserId(u64);
impl From<u64> for UserId {
fn from(value: u64) -> Self { UserId(value) }
}
impl From<UserId> for u64 {
fn from(id: UserId) -> Self { id.0 }
}
fn main() {
let id = UserId::from(42);
let raw: u64 = id.into(); // explizit zurück
assert_eq!(raw, 42);
}Damit hat der Newtype eine bequeme API zum „Reinkommen" und „Rauskommen". Aufrufer kann let x: UserId = 42.into(); schreiben.
Newtype + Deref für Method-Pass-Through
Wenn dein Newtype semantisch wie der innere Typ wirken soll (z. B. ein Wrapper über String, der zusätzlich validiert, aber die String-Methoden behalten will):
use std::ops::Deref;
pub struct Slug(String);
impl Slug {
pub fn neu(s: String) -> Result<Self, &'static str> {
if s.chars().any(|c| !c.is_alphanumeric() && c != '-') {
return Err("nur a-z, 0-9, -");
}
Ok(Slug(s))
}
}
impl Deref for Slug {
type Target = str;
fn deref(&self) -> &str { &self.0 }
}
fn main() {
let s = Slug::neu("hallo-welt".into()).unwrap();
// Alle &str-Methoden funktionieren via Deref:
assert_eq!(s.len(), 10);
assert!(s.starts_with("hallo"));
}Vorsicht mit Deref auf Newtype: die Rust API Guidelines empfehlen es nur für Smart-Pointer-artige Typen, nicht als generelle Konvertierung. Bei normalen Newtypes lieber explizite Accessor-Methoden (als_str()).
Praxis: Newtype im echten Code
Stark-typisierte IDs
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OrderId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProductId(pub u64);
pub fn order_user(_oid: OrderId) -> UserId { UserId(0) }
fn main() {
let oid = OrderId(42);
let uid = order_user(oid);
// let u2 = order_user(uid); // Compile-Fehler — UserId ist nicht OrderId
let _ = uid;
}Drei verschiedene Typen, alle intern u64. Compiler verhindert Verwechslung.
Maß-Einheiten
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Meter(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Sekunden(pub f64);
impl std::ops::Div<Sekunden> for Meter {
type Output = MeterProSekunde;
fn div(self, rhs: Sekunden) -> MeterProSekunde {
MeterProSekunde(self.0 / rhs.0)
}
}
#[derive(Debug)]
pub struct MeterProSekunde(pub f64);
fn main() {
let strecke = Meter(100.0);
let zeit = Sekunden(10.0);
let geschwindigkeit = strecke / zeit;
println!("{geschwindigkeit:?}"); // MeterProSekunde(10.0)
}Dimensionale Analyse zur Compile-Zeit — Meter / Sekunden = MeterProSekunde. Verwechslung physikalischer Größen unmöglich.
Geld als Cent-Newtype
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct EurCent(pub i64);
impl EurCent {
pub fn von_euro(eur: f64) -> Self {
EurCent((eur * 100.0).round() as i64)
}
pub fn euro_string(&self) -> String {
format!("{},{:02} €", self.0 / 100, (self.0 % 100).abs())
}
}
impl std::ops::Add for EurCent {
type Output = EurCent;
fn add(self, other: EurCent) -> EurCent { EurCent(self.0 + other.0) }
}
fn main() {
let preis = EurCent(1990);
let rabatt = EurCent(200);
let endpreis = preis + EurCent(-rabatt.0);
assert_eq!(endpreis.euro_string(), "17,90 €");
}Geld in Cent als i64 — keine Float-Rundungsfehler. Add-Impl macht arithmetische Operationen möglich.
API-Token mit Validierung
pub struct ApiToken(String);
impl ApiToken {
pub fn neu(roh: String) -> Result<Self, &'static str> {
if roh.len() < 32 || roh.len() > 128 {
return Err("Token-Länge muss 32-128 sein");
}
if !roh.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
return Err("nur a-z, 0-9, -, _ erlaubt");
}
Ok(ApiToken(roh))
}
pub fn als_str(&self) -> &str { &self.0 }
}
// Funktionen, die ApiToken nehmen, müssen sich nicht erneut validieren:
pub fn rufe_api_auf(_token: &ApiToken, _endpoint: &str) {
// Token ist garantiert valide
}Privates Feld + validierender Konstruktor — der Typ kann nur valide existieren.
Wrapper über Stdlib für Custom-Trait-Impl
use std::fmt;
pub struct Tags(Vec<String>);
impl fmt::Display for Tags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mit_hash: Vec<String> = self.0.iter()
.map(|t| format!("#{t}"))
.collect();
write!(f, "{}", mit_hash.join(" "))
}
}
impl Tags {
pub fn neu(tags: Vec<String>) -> Self { Tags(tags) }
}
fn main() {
let t = Tags::neu(vec!["rust".into(), "tutorial".into(), "newtype".into()]);
println!("{t}"); // "#rust #tutorial #newtype"
}Display für Vec<String> wäre wegen Orphan-Rule verboten. Wrapper macht es möglich — und gibt dir die Kontrolle über die Formatierung.
Sekunden vs. Millisekunden
#[derive(Clone, Copy)]
pub struct Sekunden(pub u64);
#[derive(Clone, Copy)]
pub struct Millisekunden(pub u64);
impl From<Sekunden> for Millisekunden {
fn from(s: Sekunden) -> Self { Millisekunden(s.0 * 1000) }
}
pub fn warte(ms: Millisekunden) {
std::thread::sleep(std::time::Duration::from_millis(ms.0));
}
fn main() {
warte(Millisekunden(500));
warte(Sekunden(2).into()); // implizit konvertiert
}Verwechslung von Zeiteinheiten ist eine klassische Bug-Quelle in C/C++. Newtypes mit From-Impl machen Konvertierung explizit, aber bequem.
Validierter Username
pub struct Username(String);
impl Username {
pub fn neu(s: String) -> Result<Self, &'static str> {
if s.len() < 3 || s.len() > 20 {
return Err("Username muss 3-20 Zeichen haben");
}
if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err("nur a-z, 0-9, _ erlaubt");
}
if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
return Err("Username darf nicht mit Ziffer beginnen");
}
Ok(Username(s))
}
pub fn als_str(&self) -> &str { &self.0 }
}Drei Validierungs-Regeln, alle in einer Stelle. Wer einen Username hat, muss sich um nichts mehr kümmern.
Phantom-typisierter Container
use std::marker::PhantomData;
pub struct Sortiert;
pub struct Unsortiert;
pub struct Liste<T, State> {
items: Vec<T>,
_state: PhantomData<State>,
}
impl<T: Ord> Liste<T, Unsortiert> {
pub fn neu(items: Vec<T>) -> Self {
Liste { items, _state: PhantomData }
}
pub fn sortieren(mut self) -> Liste<T, Sortiert> {
self.items.sort();
Liste { items: self.items, _state: PhantomData }
}
}
impl<T> Liste<T, Sortiert> {
pub fn binaer_suchen(&self, x: &T) -> Option<usize> where T: Ord {
self.items.binary_search(x).ok()
}
}
fn main() {
let liste = Liste::neu(vec![3, 1, 4, 1, 5]);
// liste.binaer_suchen(&3); // Compile-Fehler — nicht sortiert
let sortiert = liste.sortieren();
assert_eq!(sortiert.binaer_suchen(&3), Some(2));
}binaer_suchen ist nur auf Liste<T, Sortiert> verfügbar. Compiler verhindert Suche in unsortierter Liste.
Custom Hash für Cache-Key
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
pub struct CaseInsensitiveKey(pub String);
impl PartialEq for CaseInsensitiveKey {
fn eq(&self, other: &Self) -> bool {
self.0.to_lowercase() == other.0.to_lowercase()
}
}
impl Eq for CaseInsensitiveKey {}
impl Hash for CaseInsensitiveKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_lowercase().hash(state);
}
}
fn main() {
let mut map: HashMap<CaseInsensitiveKey, i32> = HashMap::new();
map.insert(CaseInsensitiveKey("Hallo".into()), 1);
assert_eq!(map.get(&CaseInsensitiveKey("HALLO".into())), Some(&1));
}Wrapper über String mit case-insensitive Equality und Hash. Klassisches Newtype-Anwendung für Custom-Trait-Verhalten.
Besonderheiten
Newtypes sind kostenlos zur Laufzeit.
Im Maschinencode ist UserId(42) identisch zu 42_u64. Der Compiler optimiert den Wrapper weg. Du bekommst die Typsicherheit ohne Performance-Verlust — eines der Beispiele für Zero-Cost-Abstraction.
#[repr(transparent)] garantiert Zero-Cost.
Ohne das Attribut ist Zero-Cost zwar fast immer der Fall, aber nicht garantiert. Mit #[repr(transparent)] ist es Teil der API-Garantie — wichtig bei FFI, bei Unsafe-Casts, bei stabilen ABIs.
Newtype umgeht die Orphan-Rule.
Du darfst Display nicht für Vec<T> implementieren (beide extern). Aber für deinen eigenen MyVec(Vec<T>) — eigener Typ aus deinem Crate. Klassischer Workaround.
Privates Feld + Konstruktor = garantiert valide.
Wenn das Feld privat ist und der einzige Konstruktor validiert, kann der Typ niemals einen ungültigen Wert haben. Das ist „Parse, Don't Validate" — einmal beim Konstruktor prüfen, danach im gesamten System darauf verlassen.
derive_more-Crate für Operatoren.
Wer einen Newtype über einen numerischen Typ hat und Add/Sub/Mul/Display erben will: derive_more-Crate macht das per #[derive(Add, Display)]. Sehr typisch bei Maß-Einheiten.
From + From für transparente Konvertierung.
impl From<u64> for UserId und impl From<UserId> for u64 — beide Richtungen. Damit funktioniert let id: UserId = 42.into() und let raw: u64 = id.into(). Sehr bequem.
Deref auf Newtype ist umstritten.
Die Rust API Guidelines raten zu Deref nur für Smart-Pointer-artige Typen. Bei normalen Wrappern (Email, UserId) lieber explizite Accessoren (als_str(), wert()) statt versteckte Konvertierung über *.
Newtype-Pattern ist überall in idiomatischem Rust.
Sobald du anfängst, Rust-Bibliotheken zu lesen, siehst du Newtypes überall: serde_json::Value (Wrapper über internem Enum), tokio::time::Duration (Wrapper über std::time::Duration), chrono::DateTime (Wrapper über internen Time-Type). Es ist das Standard-Pattern für „eigener Typ über bestehendem".
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Newtype Pattern
- Rust Design Patterns – Newtype
- Rust API Guidelines – Newtype
- Rust Reference – repr(transparent)
- Parse, Don't Validate