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

Rust Newtype
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:

Rust Ohne Newtype — fehleranfällig
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:

Rust Mit Newtype — typsicher
#[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!

Rust Newtype umgeht Orphan-Rule
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:

Rust Validierter Newtype
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)]:

Rust 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 zu f64-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:

Rust From-Impl
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):

Rust Deref-Pass-Through
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

Rust ID-Typen
#[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

Rust Units
#[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

Rust Money
#[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

Rust Token
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

Rust Custom Display für Vec
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

Rust Time-Units
#[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

Rust 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

Rust Phantom-typed
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

Rust 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

/ Weiter

Zurück zu Structs & Methoden

Zur Übersicht