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 syntaktisch unscheinbar — ein Tuple-Struct mit genau einem Feld, in einer Zeile deklariert. Konzeptuell ist er aber sehr mächtig. Auf der Maschinen-Ebene wirkt er wie der innere Typ: Meter hat dieselbe Größe und dasselbe Layout wie f64, kein Header, kein Padding, keine zusätzlichen Bytes. Funktions-Aufrufe mit Meter-Parameter sind binär identisch zu Aufrufen mit f64-Parameter.

Im Typ-System ist er aber ein eigener, nominaler Typ. Der Compiler sieht Meter und f64 als unterschiedlich und verhindert Verwechslungen — auch wenn ihre Repräsentation identisch ist. Diese Trennung zwischen physischer und logischer Identität ist die Grundlage aller drei Newtype-Anwendungsfälle, die in diesem Artikel folgen.

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
}

Mit den drei verschiedenen Newtype-Typen (UserId, Cents) kann der Compiler Reihenfolge-Verwechslungen direkt erkennen. Der Aufruf ueberweisen(betrag, alice, bert) mit vertauschter Parameter-Reihenfolge ist ein Compile-Fehler — die Typen Cents, UserId, UserId passen nicht zu den erwarteten UserId, UserId, Cents.

Erfundene Identifier-Typen sind eines der einfachsten und wirksamsten Werkzeuge, um eine ganze Klasse von Bugs zu vermeiden. In großen Codebasen mit vielen u64-IDs (User, Order, Product, Account, Invoice, ...) ohne Newtypes ist es nur eine Frage der Zeit, bis irgendwo die falsche ID an die falsche Stelle gerät. Mit Newtypes wird das Compile-Fehler statt Production-Bug.

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)"
}

Die Orphan-Rule ist eine wichtige Regel in Rust's Trait-System: sie verhindert, dass mehrere Bibliotheken konfliktende Trait-Implementierungen für denselben fremden Typ schreiben. Aber sie blockiert auch legitime Anwendungsfälle — etwa wenn du Display für Vec<u8> willst (mit einer Custom-Formatierung).

Die Newtype-Lösung umgeht das elegant: du wrappst den fremden Typ in einen eigenen Newtype, und auf diesem eigenen Typ darfst du jeden Trait implementieren. Aus deiner Sicht ist es transparenter Wrapper, aus Compiler-Sicht ist es ein neuer Typ aus deinem Crate.

Sehr klassische Anwendung für domain-spezifische Custom-Display-Formatierungen — etwa „mein MeineGeldVec(Vec<EurCent>) soll als formatierte Liste mit Euro-Zeichen ausgegeben werden". Ohne Newtype geht das nicht (Vec ist fremd, Display ist fremd); mit Newtype trivial.

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, String> {
        if !s.contains('@') || !s.contains('.') {
            return Err(String::from("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());
}

Der Knackpunkt ist das private Feld. Wer außerhalb des Moduls eine Email erstellen will, kann nicht einfach Email(string) schreiben — das Feld ist nicht öffentlich. Der einzige Weg führt über Email::neu(...), das die Validierung enthält. Damit hat das Typ-System eine starke Garantie: jede Email-Instanz, die irgendwo im System existiert, ist garantiert eine valide Email-Adresse.

Dieses Pattern heißt „Parse, Don't Validate" und ist eines der wirkungsvollsten in stark typisiertem Code. Statt überall im Code zu fragen „ist diese Email valide?" (mit dem Risiko, mal zu vergessen), validierst du einmalig beim Konstruktor und verlässt dich danach auf den Typ. Alle Funktionen, die Email als Parameter erwarten, können direkt damit arbeiten — kein Defensiv-Code, keine Re-Validation.

In der Praxis ist das Pattern überall zu finden: NonEmptyString, PositiveInt, ValidatedConfig, AuthenticatedRequest. Jeder dieser Typen ist ein Newtype mit Konstruktor-Validierung, dessen Existenz die Validität beweist.

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 die Identität des Layouts faktisch fast immer gegeben — der Compiler hat keine Inzentive, einen Single-Field-Tuple-Struct anders zu repräsentieren als sein inneres Feld. Aber: es ist nicht garantiert vom Compiler, und in zukünftigen Optimierungen könnte sich das theoretisch ändern.

#[repr(transparent)] macht die Layout-Identität zur API-Garantie und ist in drei Fällen wichtig:

FFI mit C — wenn du Meter-Werte über eine C-Schnittstelle reichst, müssen sie binär-kompatibel zu f64 sein. Eine C-Funktion void process(double*) muss mit einem *const Meter aufgerufen werden können. repr(transparent) garantiert das.

Stable ABI über Bibliotheks-Versionen — wenn du eine Library mit pub struct Meter(f64) veröffentlichst und andere Bibliotheken sich auf das Layout verlassen (etwa für Unsafe-Casts), schützt repr(transparent) die Abwärts-Kompatibilität.

Unsafe-Casting zwischen Wrapper und Innerem — wenn du in einem unsafe-Block zwischen &Meter und &f64 casten willst, brauchst du die Layout-Garantie, sonst ist das Undefined Behavior. Bei normalem Safe-Rust-Code kommt das selten vor.

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, String> {
        if s.chars().any(|c| !c.is_alphanumeric() && c != '-') {
            return Err(String::from("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, String> {
        if roh.len() < 32 || roh.len() > 128 {
            return Err(String::from("Token-Länge muss 32-128 sein"));
        }
        if !roh.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') {
            return Err(String::from("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, String> {
        if s.len() < 3 || s.len() > 20 {
            return Err(String::from("Username muss 3-20 Zeichen haben"));
        }
        if !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
            return Err(String::from("nur a-z, 0-9, _ erlaubt"));
        }
        if s.chars().next().is_some_and(|c| c.is_ascii_digit()) {
            return Err(String::from("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

Dieses Muster nutzt Generics und PhantomData. Beide Themen kommen erst in späteren Kapiteln — der Code wird hier zum Wiederfinden gezeigt, ein voller Durchstieg lohnt erst nach den Generics- und Lifetime-Kapiteln.

Rust Phantom-typed
use std::marker::PhantomData;

pub struct Sortiert;
pub struct Unsortiert;

pub struct ListeI32<State> {
    items: Vec<i32>,
    _state: PhantomData<State>,
}

impl ListeI32<Unsortiert> {
    pub fn neu(items: Vec<i32>) -> Self {
        ListeI32 { items, _state: PhantomData }
    }

    pub fn sortieren(mut self) -> ListeI32<Sortiert> {
        self.items.sort();
        ListeI32 { items: self.items, _state: PhantomData }
    }
}

impl ListeI32<Sortiert> {
    pub fn binaer_suchen(&self, x: &i32) -> Option<usize> {
        self.items.binary_search(x).ok()
    }
}

fn main() {
    let liste = ListeI32::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 ListeI32<Sortiert> verfügbar. Der Compiler verhindert eine Suche in einer unsortierten Liste. Die generische Variante (für beliebige T: Ord) ist eine kleine Erweiterung im Generics-Kapitel.

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