WEP: Newtype Semantics
Decision
type T = U uses newtype semantics:
Tis a distinct type fromUTinherits all methods, operators, and traits fromUTandUshare the same runtime representation (zero-cost abstraction)- Explicit cast (
as) is required to convert betweenTandU - Literal coercion to
Tis allowed when type context expectsT
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:
selfparameter type- Other parameters of the base type
- Return type
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:
- Hide the base type's methods
- Control exactly which operations are allowed
- Have different runtime behavior
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
- Type safety: Prevents mixing incompatible types that share representation
- Zero cost: No runtime overhead, same Wasm output
- Ergonomic: Methods and operators work naturally
- Extensible: Can add newtype-specific methods via
impl - Consistent with WASI: Types like
InstantandDurationbecome truly distinct
Negative
- More verbose: Need explicit casts where implicit conversion worked before
- 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:
- Distinct-type checking, method/operator/trait inheritance, and zero-cost shared representation
- Newtype-specific inherent methods via
impl, including inside generic function bodies - Method-signature substitution, extended to generic container return types (
Option<Base>→Option<Newtype>,List<Base>→List<Newtype>) - Trait bounds (
fn compare<T: Ord>(a: T, b: T)accepts a newtype), andList<Newtype>.sort()via inheritedOrd - Associated-function inheritance (
ByteList::with_capacity) with newtype return FromIteratorinheritance, socollect()targets a newtype directly (let bytes: ByteList = s.bytes().collect(), wado-lang/wado#1623)- Generic newtypes (
type MyArray<T> = List<T>) - Newtypes over the raw GC
Array<T>inheritIndextraits and associated functions (ByteArray) for-ofinheritsIntoIterator, both by value (Item = T) and by reference (Item = &T)
See Also
- Struct and Trait System - struct wrapper alternative
- Literal Type Conversion Rules - literal coercion
