Types and Traits

Guideline: Use strong types to differentiate between logically distinct values gui_xztNdXA2oFNC
status: draft
tags: types, safety, understandability
category: advisory
decidability: undecidable
scope: module
release: 1.85.0;1.85.1

Parameters and variables with logically distinct types must be statically distinguishable by the type system.

Use a newtype (e.g., struct Meters(u32);) when:

  • Two or more quantities share the same underlying primitive representation but are logically distinct

  • Confusing them would constitute a semantic error

  • You need to improve type safety and encapsulation

  • You need to enable trait-based behavior

  • You need to establish new invariants

Rationale: rat_kYiIiW8R2qD2
status: draft
parent needs: gui_xztNdXA2oFNC

This rule ensures that parameters and variables convey intent directly through the type system to avoid accidental misuse of values with identical primitives but different semantics. In particular:

  • Prevents mixing logically distinct values. Primitive types like u32 or u64 can represent lengths, counters, timestamps, durations, IDs, or other values. Different semantic domains can be confused, leading to incorrect computations. The Rust type system prevents such mistakes when semantics are encoded into distinct types.

  • Improves static safety. Statically distinct types allow the compiler to enforce domain distinctions. Accidental swapping of parameters or returning the wrong quantity becomes a compile-time error.

  • Improves readability and discoverability. Intent-revealing names (Meters, Seconds, UserId) make code self-documenting. Type signatures become easier to read and understand.

  • Enables domain-specific trait implementations. Statically distinct types allow you to implement Add, Mul, or custom traits in ways that match the domain logic. Aliases cannot do this, because they are not distinct types.

  • Supports API evolution. Statically distinct types act as strong API contracts that can evolve independently from their underlying representations.

Non-Compliant Example: non_compl_ex_PO5TyFsRTlWw
status: draft
parent needs: gui_xztNdXA2oFNC

This noncompliant example uses primitive types directly, leading to potential confusion between distance and time. Nothing prevents the caller from passing time as distance or vice-versa. The units of each type are not clear from the function signature alone. Mistakes compile cleanly and silently produce wrong results.

fn travel(distance: u32, time: u32) -> u32 {
   distance / time
}

fn main() {
   let d = 100;
   let t = = 10;
   let _result = travel(t, d);  // Compiles, but semantically incorrect
}
Non-Compliant Example: non_compl_ex_PO5TyFsRTlWv
status: draft
parent needs: gui_xztNdXA2oFNC

This noncompliant example uses aliases instead of distinct types. Aliases do not create new types, so the compiler cannot enforce distinctions between Meters and Seconds.

Aliases cannot do this, because they are not distinct types. This noncompliant example uses primitive types directly, leading to potential confusion between distance and time. Nothing prevents the caller from passing time as distance or vice-versa. The units of each type are not clear from the function signature alone. Mistakes compile cleanly and silently produce wrong results.

type Meters = u32;
type Seconds = u32;
type MetersPerSecond = u32;

fn travel(distance: Meters, time: Seconds) -> MetersPerSecond {
   distance / time
}

fn main() {
   let d: Meters = 100;
   let t: Seconds = 10;
   let _result = travel(t, d);  // Compiles, but semantically incorrect
}
Compliant Example: compl_ex_WTe7GoPu5Ez1
status: draft
parent needs: gui_xztNdXA2oFNC

This compliant example uses newtypes to create distinct types for Meters, Seconds, and MetersPerSecond. The compiler enforces correct usage, preventing accidental swapping of parameters. The function signature clearly conveys the intended semantics of each parameter and return value.

use std::ops::Div;

#[derive(Debug, Clone, Copy)]
struct Meters(u32);

#[derive(Debug, Clone, Copy)]
struct Seconds(u32);

#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(u32);

impl Div<Seconds> for Meters {
    type Output = MetersPerSecond;

    fn div(self, rhs: Seconds) -> Self::Output {
         MetersPerSecond(self.0 / rhs.0)
    }
 }

 fn main() {
     let d = Meters(100);
     let t = Seconds(10);
     let result = d / t;  // Clean and type-safe!
     println!("{:?}", result);  // MetersPerSecond(10)
 }