#180 Jun 3, 2026

180. Option::unzip — Split an Optional Pair Into a Pair of Options

When you have an Option<(A, B)> but the rest of your code wants (Option<A>, Option<B>), the obvious move is two map calls. Option::unzip does both halves in a single call.

The two-map dance

Say a parser returns Option<(&str, i32)> — a name and an age, or nothing. Downstream you want them as separate optional columns:

1
2
3
4
5
6
7
let parsed: Option<(&str, i32)> = Some(("ada", 36));

let name = parsed.map(|(n, _)| n);
let age  = parsed.map(|(_, a)| a);

assert_eq!(name, Some("ada"));
assert_eq!(age,  Some(36));

That’s two passes over the same Option, two closures that throw away half their input, and an easy place to typo n for a. Option::unzip collapses it:

1
2
3
4
5
6
let parsed: Option<(&str, i32)> = Some(("ada", 36));

let (name, age) = parsed.unzip();

assert_eq!(name, Some("ada"));
assert_eq!(age,  Some(36));

Some((a, b)) becomes (Some(a), Some(b)). No closures, no destructuring noise.

The None case is the whole point

unzip shines on the None branch. None becomes (None, None) — both sides empty, no special-casing:

1
2
3
4
5
let missing: Option<(&str, i32)> = None;
let (name, age) = missing.unzip();

assert_eq!(name, None);
assert_eq!(age,  None);

Compare with the manual version: you’d still write two maps and rely on each one to short-circuit. Same behavior, more lines, more chances to drift apart when someone edits one branch and forgets the other.

Round-trip with zip

Option::zip is the inverse — it builds Option<(A, B)> from (Option<A>, Option<B>), returning Some only if both are Some:

1
2
3
4
5
6
7
8
9
let name: Option<&str> = Some("ada");
let age: Option<i32>   = Some(36);

let joined = name.zip(age);
assert_eq!(joined, Some(("ada", 36)));

let (n, a) = joined.unzip();
assert_eq!(n, Some("ada"));
assert_eq!(a, Some(36));

One mental model: Option<(A, B)> and (Option<A>, Option<B>) are almost the same shape, and these two methods let you flip between them without match.

When to reach for it

Any time a single optional value naturally carries two parts that the rest of the code wants to handle independently — coordinates, key/value pairs, name/age, parsed/raw. If you find yourself writing opt.map(|(x, _)| x) and opt.map(|(_, y)| y) back-to-back, that’s the signal.

Stable since Rust 1.66.

← Previous 179. Iterator::max_by_key — Find the Best Element Without a Manual Fold Next → 181. Option::get_or_insert_with — Lazy Default That Returns &mut