In praise of exhaustive destructuring | Antoine Vandecrème
In praise of exhaustive destructuring
2026-07-12
When I learned Rust, I was frustrated by the fact that one had to list all fields or use the .. syntax when destructuring a struct.
To illustrate this and because I am writing this during a heatwave, let's suppose we have the following struct:
struct WeatherReading {<br>station_id: String,<br>recorded_at: DateTime,<br>temperature: f64,<br>humidity: f64,<br>pressure: f64,
Note : Except for recorded_at, all fields should be newtypes. Not done here for brevity.
Now I just want to retrieve station_id and recorded_at, why should I need to write:
let WeatherReading {<br>station_id,<br>recorded_at,<br>..<br>} = weather_reading;
In TypeScript, I could just do:
const { station_id, recorded_at } = weather_reading;
And in Haskell:
-- With NamedFieldPuns extension<br>let WeatherReading {<br>station_id,<br>recorded_at,<br>} = weather_reading
-- Or with RecordWildCards<br>let WeatherReading {..} = weather_reading
Not that much of a hassle, but that made me favor the weather_reading.station_id/weather_reading.recorded_at syntax.
However, in the past few months, I have started to greatly favor struct destructuring over accessing fields with the . syntax because that makes maintaining the software safer.
For example, we want to detect dangerous weather:
fn is_dangerous(weather_reading: &WeatherReading) -> bool {<br>if weather_reading.temperature > 40.0 { // assuming °C, that's 104°F or 313.15K<br>return true;<br>if weather_reading.humidity 5.0 { // assuming percentage<br>return true;<br>if weather_reading.pressure 960.0 { // assuming hPa, that's 28.35 inHg<br>return true;<br>return false;
That function is dangerous! To see why, let's suppose some stations now have an anemometer and are able to record wind speed. So, we add a new wind_speed field to the WeatherReading struct:
struct WeatherReading {<br>station_id: String,<br>recorded_at: DateTime,<br>temperature: f64,<br>humidity: f64,<br>pressure: f64,<br>wind_speed: Optionf64>, // Again, this should be a newtype
All good! Let's ship!
Uh oh. Did you remember to update the is_dangerous function? Surely heavy wind should be reported as dangerous. Unfortunately, the Rust compiler did not warn us about it.
The good news is that we could have been warned if we wrote the function like this:
fn is_dangerous(<br>WeatherReading {<br>station_id: _,<br>recorded_at: _,<br>temperature,<br>humidity,<br>pressure,<br>}: &WeatherReading<br>) -> bool {<br>if *temperature > 40.0 { // assuming °C, that's 104°F or 313.15K<br>return true;<br>if *humidity 5.0 { // assuming percentage<br>return true;<br>if *pressure 960.0 { // assuming hPa, that's 28.35 inHg<br>return true;<br>return false;
We would now get error[E0027]: pattern does not mention field 'wind_speed'.
Note that you must be explicit about unused fields and resist the temptation of using the .. pattern for this trick to work.
Because self can't be destructured from the arguments list in Rust, if is_dangerous was written as a method, a bit of ceremony is necessary:
impl WeatherReading {<br>fn is_dangerous(&self) -> bool {<br>let WeatherReading {<br>station_id: _,<br>recorded_at: _,<br>temperature,<br>humidity,<br>pressure,<br>} = self;<br>if *temperature > 40.0 { // assuming °C, that's 104°F or 313.15K<br>return true;<br>if *humidity 5.0 { // assuming percentage<br>return true;<br>if *pressure 960.0 { // assuming hPa, that's 28.35 inHg<br>return true;<br>return false;
I find this trick especially useful when writing From implementations between the different layers (data access, business logic, API) of a CRUD web service.<br>If you add a field in one layer, the compiler will force you to decide whether that field should be propagated to the other layers or not.
Another advantage is that if you see the same bunch of fields always destructured together while the others are ignored, that can be a good indication that maybe those fields should be extracted into their own struct.
If you are using TypeScript, there is a trick using the Required type.
For Haskell, there is surprisingly no solution at the moment, though there is a proposal.