3 unstable releases
0.2.0 | Apr 13, 2024 |
---|---|
0.1.0 | Apr 1, 2024 |
0.1.0-pre.1 | Apr 29, 2023 |
#699 in Rust patterns
85 downloads per month
14KB
205 lines
bty
Streamlined definition and usage of branded types in Rust.
This crate provides the Brand
type and the brand!
macro, which can be used
to declare and seamlessly use branded types in Rust.
[dependencies]
bty = "0.1"
Supports rustc 1.60+
Example
The brand!
macro may be used to declare branded types, which will be
discriminated based on the name of the type alias.
bty::brand!(
type UserId = i32;
);
Instances of UserId
may be constructed using one of the deserialization
implementations, such as the serde
one or the sqlx
one. Manually
instantiation, though unrecommended, can be done using the unchecked_from_inner
associated function.
See this thread from Matt Pocock on Twitter for a more exemplified and intuitive view. Though it shows examples in TypeScript, the principles remain the same.
Rationale
It's not rare to have values that, although of the same type, belong to
different domains. For example, a web application could use the i32
type to
represent both user ids and order ids.
While this may seem reasonable, since those different domain types have the same type, one could easily pass a user id to a function expecting an order id.
Since Rust's type system is nominal, this problem could be avoided by introducing different types for each id. For example, one could have:
pub struct UserId(i32);
pub struct OrderId(i32);
Now the compiler statically ensures that a user id is never erroneously passed in place of an order id. Nice!
Though this approach suits most cases, it gets unwieldy as the number of custom
id types grows since, for usability's sake, the type definition alone is rarely
sufficient. For example, to Clone
or Debug
a custom id, one must implement
those traits for all of the custom types.
#[derive(Clone, Debug)]
pub struct UserId(i32);
#[derive(Clone, Debug)]
pub struct OrderId(i32);
The problem worsens as the number of uses for the id types grows. For example,
what about serde
serialization and deserialization?
bty
solves this problem by not having separate types for the branded types.
Instead, a single Brand
type is used. Defined as Brand<Tag, Inner>
, it is
generic over a Tag
type, which discriminates values of different "brands"
(i.e., domains) and the underlying type, represented by Inner
.
For most Rust's commonly used traits, if Inner
implements it, then so does
Brand
. This means if Inner
implements Clone
and Debug
, Brand<_, Inner>
will also have them implemented.
Following the previous example, one could use bty
and have:
bty::brand!(
pub type UserId = i32;
pub type OrderId = i32;
);
There's nothing special with the i32
type. Just like manually defined structs,
any type may be used to construct a branded type.
License
MIT License.
Dependencies
~0.1–1.8MB
~34K SLoC