How hard do you lean on Rust type system to encode your logic and constraints?
In a recent episode of the Rust in Production Podcast, the guests spoke a lot about encoding business logic and domain constraints via the type system:
- Newtypes with private fields + smart constructors — wrap a primitive (NonEmpty<T>, Email, Port) and only let it be built via a function that validates. The invariant holds for the rest of the program by construction.
- Zero-sized types as proofs: struct AdminProof(()) with a private field. Functions that need authorization take &AdminProof, and only auth::check_admin() can mint one. Compile-time
capability, zero runtime cost. - Typestate via PhantomData<S>: Connection<Disconnected> -> Connection<Connected> -> Connection<Authenticated>. Methods only exist on the right state, so misuse (send on a Disconnected) won't compile.
- Phantom units / phantom tags: Length<Meters> vs Length<Feet>, Id<User> vs Id<Order>. Same runtime layout, different types — mixing them is a compile error.
It sounds super awesome. However, I can see that it requires extra mental effort from the developer to come up with the right types. There's also an ongoing cost when working with these types, you often need to convert one thing into another, or figure out how to use the API in the first place.
So I'm wondering, what do people actually do on production projects? Are you more of a Go / Java / C# kind of developer, sticking to basic structs, or do you actually express constraints in the type system? (I've never really tried this in production code). Which techniques do you use?