Wado

WEP: Newtype Semantics

Decision

type T = U uses newtype semantics:

  1. T is a distinct type from U
  2. T inherits all methods, operators, and traits from U
  3. T and U share the same runtime representation (zero-cost abstraction)
  4. Explicit cast (as) is required to convert between T and U
  5. Literal coercion to T is allowed when type context expects T

Basic Usage

type Meters = f64;
type Kilometers = f64;

let m: Meters = 1000.0;       // OK: literal coercion
let km: Kilometers = 1.0;     // OK: literal coercion

let sum = m + m;              // OK: Meters + Meters -> Meters
let bad = m + km;             // ERROR: cannot mix Meters and Kilometers

let raw: f64 = m as f64;      // OK: explicit cast
let converted = (m as f64) / 1000.0 as Kilometers;  // OK: explicit conversion

Method Signature Substitution

When calling a method on a newtype, the method signature is substituted to use the newtype instead of the base type. This applies to:

type Duration = u64;

// u64 has: fn saturating_add(&self, other: u64) -> u64
// When called on Duration, becomes: fn saturating_add(&self, other: Duration) -> Duration

let d1: Duration = 1000;
let d2: Duration = 2000;
let d3 = d1.saturating_add(d2);  // d3: Duration (not u64)

let raw: u64 = 500;
let bad = d1.saturating_add(raw);  // ERROR: expected Duration, got u64

This is critical for user-defined types like i128:

// i128 might be defined as a tuple internally
type i128 = [i64, i64];

impl i128 {
    fn add(&self, other: &i128) -> i128 { ... }
}

let a: i128 = ...;
let b: i128 = ...;
let c = a + b;  // c: i128 (not [i64, i64])

Newtype-Specific Implementations

You can add methods specific to a newtype via impl:

type Radians = f64;
type Degrees = f64;

impl Radians {
    fn to_degrees(&self) -> Degrees {
        return (*self * 180.0 / 3.14159) as Degrees;
    }
}

impl Degrees {
    fn to_radians(&self) -> Radians {
        return (*self * 3.14159 / 180.0) as Radians;
    }
}

let r: Radians = 3.14159;
let d = r.to_degrees();  // d: Degrees

let x: f64 = 1.0;
x.to_degrees();  // ERROR: f64 has no method 'to_degrees'

Associated Functions Are Inherited

A newtype inherits its base type's associated (static) functions, and the result is viewed as the newtype (they share a representation):

type ByteList = List<u8>;

let mut b = ByteList::with_capacity(16);   // dispatches to List::<u8>::with_capacity
b.push(0xff);                              // b: ByteList
let filled = ByteList::filled(4, 0);       // filled: ByteList

When the base function's return type is its own type (-> Self / -> List<T>), the call yields the newtype; the base's concrete type arguments (here u8) seed the instantiation, so no turbofish is needed.

Base Type Implementations Are Inherited

When you add methods to the base type, all newtypes derived from it can use those methods:

type Meters = f64;
type Seconds = f64;

impl f64 {
    fn is_positive(&self) -> bool {
        return *self > 0.0;
    }
}

let m: Meters = 100.0;
let s: Seconds = -5.0;

m.is_positive();  // OK: true
s.is_positive();  // OK: false

Trait Inheritance

Newtypes inherit all trait implementations from the base type:

type Duration = u64;

// u64 implements Eq and Ord
// Therefore Duration also implements Eq and Ord

let d1: Duration = 1000;
let d2: Duration = 2000;

d1 == d2;         // OK: false
d1 < d2;          // OK: true

fn compare<T: Ord>(a: T, b: T) -> bool { ... }
compare(d1, d2);  // OK: Duration satisfies Ord bound

Iteration Is Inherited

for-of desugars to IntoIterator, a trait, so a newtype over an iterable inherits it. The associated iterator/item types resolve against the base's type args (peeled from the newtype), and a &newtype receiver reaches the base's by-reference impl (impl IntoIterator for &List<T>, Item = &T):

type ByteList = List<u8>;

let bytes: ByteList = ...;
for let b of bytes {  b: u8   }   // impl IntoIterator for List<u8>
for let b of &bytes { b: &u8  }   // impl IntoIterator for &List<u8>

Cast Rules

From To Allowed
Newtype T Base type U Yes, via as
Base type U Newtype T Yes, via as
Newtype T Newtype S (same base) Yes, via as
List<T> List<U> No
&T &U Yes, via as
Fn(T) Fn(U) No

Chained Newtypes

type A = i32;
type B = A;
type C = B;

let c: C = 1;
let b = c as B;    // OK
let a = c as A;    // OK: direct cast through chain
let i = c as i32;  // OK: direct cast to base

Generic Newtypes

type MyArray<T> = List<T>;

let arr: MyArray<i32> = [1, 2, 3];
arr.len();                        // OK: inherits List methods
arr.push(4);                      // OK

let plain: List<i32> = arr as List<i32>;  // ERROR: generic cast not allowed

What Newtypes Do NOT Provide

For complete type isolation where you want to:

Use a struct wrapper instead:

struct UserId {
    value: i32,
}

impl UserId {
    fn new(value: i32) -> UserId {
        return UserId { value };
    }

    fn get(&self) -> i32 {
        return self.value;
    }
}

Consequences

Positive

  1. Type safety: Prevents mixing incompatible types that share representation
  2. Zero cost: No runtime overhead, same Wasm output
  3. Ergonomic: Methods and operators work naturally
  4. Extensible: Can add newtype-specific methods via impl
  5. Consistent with WASI: Types like Instant and Duration become truly distinct

Negative

  1. More verbose: Need explicit casts where implicit conversion worked before
  2. Method return types: Substitution may be surprising (returns newtype, not base)

Comparison with Other Languages

Language type T = U Semantics
Rust Alias (use struct T(U) for newtype)
Haskell type = alias, newtype = newtype
Go Distinct type (like this proposal)
TypeScript Alias (use branded types for distinction)
Wado Newtype (this proposal)

Capabilities

Newtypes support the full feature set today:

See Also