Type aliases that are not aliases?

Very often in my code I do stuff like:

type Address = String;
type Id = String;

etc.

This works well, and helps type names to better document the intention. However it does not prevent accidentally passing Address as Id.

There is a newtype approach to fix that (with crates like newtype-derive and shrinkwraprs), and I tried it, but it's actually a PITA. Relying on Deref etc. is just very not ergonomic. I want Address to be exactly like String, not "kinda-String".

All I really want is to be able to define type alias that actually defines a new separate type, exactly like the original, but that can't be accidentally mixed.

Are there any plans to support it? Any workarounds (that aren't newtype-based) that I could try?

I haven't seen any suggestions to go beyond the current newtype pattern.
There are crates that help copying implementations, so that you don't have to rely on deref.

I've always wanted that as well, but I think it's just harder than one thinks. Especially wrt to traits, this might not even be fully solvable.

Any examples/recommendations of crates like this?

In most cases all I want is to make an integer or a string some form of an ID. I don't really care much about traits / plan to add new ones.

I know I've wanted this as well, but if you think through it, it's just not feasible.

Let's take for example a String-but-not-String, let's name it Name. You say it should be "exactly like String", but the problem is that Rust doesn't know where you actually want this. For example, the new type would have name.replace("x", "y"), but what would it return? A Name? Would Rust need to replace String by Name in any method that returns a String? But what's so special about methods of String, why not replace create a copy of every function that takes and returns a String or &String? Same for operators: is name + "x" a Name or String? Next can of worms is Deref: why would replace even accept a &str, if Name is distinct from String. Etc.

If you say "of course replace or + would still return a String", you're losing the premise that you can use the Name "just like a String": you'd have to wrap the outcome of basic operations manually anyway. So there's not so much gain compared to the current struct newtype.

For this, it doesn't sound like you need so many operations that either implementing proxies, or doing manual wrapping and unwrapping is such a pain.

5 Likes

That's a really interesting point. Thank you.

I would be fine with it. Still better than newtypes. :smiley:

The thing is: all I really want is to be hard to pass Id where Address was expected etc. I don't want any new behaviors etc. I want a String, with all the methods and behavior. Except I can't pass as function argument to function that expect a different kind of String. With newtypes, every time I want to print Address I have to prefix it with * to trigger Deref etc.

1 Like

Sure, I get it. But the more you think about it, the more you'll realize it would require Rust to read your mind as for what operations should not work, work with the original type, and work with the new type :slight_smile:

I don't think so. All I want is a distinct kind of the base type. Everything else keep being just like the base type. You can still pass Payment as &str or String etc. All that is forbidden is passing Payment as Id directly, so in my APIs I can enforce the "kind" to avoid accidental mistakes and increase the type-level documentation.

New kinds wouldn't define a new type, so it would be impossible to define new methods, impls etc. on them. For all other purposes they keep being a String.

It appears that you are trying to add an orthogonal dimension to existing types. Perhaps crates like uom have part of the solution you want.

1 Like

Sometimes I simulate named arguments by making a struct function. This solves many of the same problems as newtypes would, by making the names part of the interface.

pub struct SomeFunction {
    id: String,
    address: String,
}

impl SomeFunction {
    pub fn call(self) -> Output {
        let Input { id, address } = self;
        ...
    }
}

// Somewhere else:
// It's impossible to mix up the argument order
assert_eq!(
    SomeFunction { id, address }.call(),
    SomeFunction { address, id }.call(),
);

But that's for even harsher cases than those that can be reasonably solved with newtypes. I don't think you'll find anything less syntactically demanding than newtypes.

2 Likes

That's a useful tip, but I have a multiple structs with multiple methods and fields, and I just want to avoid accidentally misplacing data. It's not a big deal really, and tests would catch that quickly - part of the reason, why newtype approach seems too cumbersome. But still ... if type system could help, that would be awesome.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.