In general, the language cannot simply do this for you by (for example) allowing #[repr(packed)] on enums or adding uN types for all possible Ns because of alignment requirements. In particular, enums allow taking references to the fields inside them, and (unlike raw pointers) an unaligned &T is insta-UB. That means a u14 would still need to be layed out on two-word boundaries, and an enum containing u14s would still need some padding bits between them and the tag/discriminant. This is why uN types are mostly handled by crates; at the language level they end up being barely distinguishable from u16 and friends in practice.
So the only fully general answer to "how do you do this?" is "manually", because you have to throw out some of the features regular Rust enums provide to make your optimally packed layout work, and only you can decide which of those features your code can live without. The previous answers are good examples of how you might do this manually.