16

tl;dr in Rust, is there a "strong" type alias (or typing mechanism) such that the rustc compiler will reject (emit an error) for mix-ups that may be the same underlying type?

Problem

Currently, type aliases of the same underlying type may be defined

type WidgetCounter = usize;
type FoobarTally = usize;

However, the compiler will not reject (emit an error or a warning) if I mistakenly mix up instances of the two type aliases.

fn tally_the_foos(tally: FoobarTally) -> FoobarTally {
    // ...
    tally
}

fn main() {
    let wc: WidgetCounter = 33;
    let ft: FoobarTally = 1;

    // whoops, passed the wrong variable!
    let tally_total = tally_the_foos(wc);
}

(Rust Playground)

Possible Solutions?

I'm hoping for something like an additional keyword strong

strong type WidgetCounter = usize;
strong type FoobarTally = usize;

such that the previous code, when compiled, would cause a compiler error:

error[E4444]: mismatched strong alias type WidgetCounter,
              expected a FoobarTally

Or maybe there is a clever trick with structs that would achieve this?

Or a cargo module that defines a macro to accomplish this?



I know I could "hack" this by type aliasing different number types, i.e. i32, then u32, then i64, etc. But that's an ugly hack for many reasons.


Is there a way to have the compiler help me avoid these custom type alias mixups?

JamesThomasMoon
  • 6,169
  • 7
  • 37
  • 63

1 Answers1

13

Rust has a nice trick called the New Type Idiom just for this. By wrapping a single item in a tuple struct, you can create a "strong" or "distinct" type wrapper.

This idiom is also mentioned briefly in the tuple struct section of the Rust docs.

The "New Type Idiom" link has a great example. Here is one similar to the types you are looking for:

// Defines two distinct types. Counter and Tally are incompatible with
// each other, even though they contain the same item type.
struct Counter(usize);
struct Tally(usize);

// You can destructure the parameter here to easily get the contained value.
fn print_tally(Tally(value): &Tally) {
  println!("Tally is {}", value);
}

fn return_tally(tally: Tally) -> Tally {
  tally
}

fn print_value(value: usize) {
  println!("Value is {}", value);
}

fn main() {
  let count: Counter = Counter(12);
  let mut tally: Tally = Tally(10);

  print_tally(&tally);
  tally = return_tally(tally);

  // This is a compile time error.
  // Counter is not compatible with type Tally.
  // print_tally(&count);

  // The contained value can be obtained through destructuring
  // or by potision.
  let Tally(tally_value ) = tally;
  let tally_value_from_position: usize = tally.0;

  print_value(tally_value);
  print_value(tally_value_from_position);
}
scupit
  • 704
  • 6
  • 6
  • 4
    As an added benefit, you can use this pattern to implement foreign traits on foreign types. – Aiden4 Oct 05 '21 at 02:14
  • 6
    Unfortunately, changing existing code to/from using this idiom requires adding/removing the `.0`. It would be nice if the `.0` could be inferred for one-field tuple struct automatically ("un-tuple-struct coercion"?) in order to have the ergonomics of `type` with the type safety/strictness of a tuple struct. – BallpointBen Oct 10 '21 at 04:46
  • 2
    @BallpointBen I don't agree. I see your point that changing existing code would be easier, but not having the `.0` would defeat the purpose of this idiom: the `.0` is an explicit transition between the wrapper and its underlying type. Implicit coercion could create ambiguous-looking code, and allow for the very same kind of mistakes to go unnoticed that this question seeks to prevent. – FZs Aug 06 '22 at 21:34
  • 1
    Unfortunately you need to do this destructuring, so it's not quite the same as a "strong type". I believe Golang has such a "strong type". Would be nice to have it in Rust as well. – Erik Bongers Aug 24 '23 at 19:22