Option<T>
Model a value that might not be there, and work with it without null checks.
Option<T> says a value might not be there. It has exactly two shapes:
Some<T>— there is a value.None<T>— there is not.
That is the whole type. What makes it worth using is that you cannot read the value without acknowledging the second case, so the compiler catches what a null check would have let through.
Why not just use null?
null tells you nothing. It does not say why the value is missing, or whether it was ever meant to be there. It spreads guard clauses through your code, and the compiler will still happily let you dereference it.
Option<T> says the absence out loud, in the signature, where you already look.
There is a second difference that matters more than it sounds. None is not an error. When you write:
Option<Character> FindCharacter(string name);you are not saying "this might blow up". You are saying "this might not find anything, and that is a normal outcome". If you need to know why it failed, reach for Result<T, E> instead.
Create one
Option<string> some = Option.Some("Keyleth");
Option<string> none = Option.None<string>();
Option<string> fromNullable = Option.FromNullable(sigil);
Option<string> fromTry = Option.Try(() => sigil!.Split('@')[1]);Option.SomeandOption.None<T>are the two you will write most.Option.FromNullabletakes something that might already benull— usually at the edge of your code, where you cannot control the shape.Option.Tryruns a function that might throw and gives youNoneif it does.
Transform it
You rarely want to look inside an Option. You want to keep working, and let the None case take care of itself.
Map
Map changes the value if there is one, and does nothing if there is not.
AndThen
Use AndThen when the next step also returns an Option. Map would give you an Option<Option<T>>; AndThen keeps it flat.
This was called FlatMap before 5.4.0. It was [Obsolete] through 5.x and 6.0.0 removed it, so a call to it is CS0117 rather than a warning. WM2014, the rule that reported each call site, retired with it — delete any .editorconfig entry for that id. See Deprecations.
Filter
Filter keeps a value only if it passes your predicate. If it does not, you get None.
This is the replacement for an if-guard in the middle of a pipeline.
Chain them
Put those three together and the whole thing reads top to bottom, with no branching at all:
Compare that with the version you would otherwise write:
Same behaviour. One of them tells you what it is doing.
Get the value back out
Every chain ends somewhere. These are the ways out.
Match
Match is the honest one. You supply both branches and get a plain value.
Unwrap with a fallback
Use UnwrapOr when the fallback is already sitting there. Use UnwrapOrElse when producing it costs something — the function only runs on a None.
Wrapping a value you already hold in a lambda gets you the worst of both. The fallback is built either way, and the call allocates a delegate to defer work that has already happened. If you can write it as an argument, pass it as one.
There is also a bare Unwrap(). It throws on a None. It exists for the cases where absence really is a bug, and the analyzer will tell you when you have reached for it out of habit.
Pattern matching
Since 7.0.0, Option<T> deconstructs, so C# pattern matching works on it directly:
And exhaustively, in a switch:
Check without unwrapping
Sometimes you only want a bool.
IsSomeAnd— there is a value and it passes the predicate.IsNoneOr— there is no value, or the one there passes.
Combine two options
Zip, ZipWith and Unzip
Zip pairs two options into one, and gives None if either side is missing.
ZipWith does the same but combines the two values yourself instead of making a tuple:
Unzip reverses a Zip:
A component that happens to equal its type's default is an ordinary value here, so Option.Some((0, "x")).Unzip() gives (Some(0), Some("x")). This threw before 6.0.0.
Fallback chains
Or takes the first Some it finds. OrElse is the same, but the fallback is only built if it is needed.
The rest
Three more exist, and each is occasionally exactly what you want.
And
Returns the second option, but only if the first was Some. Answers "did both arrive?"
Reduce
Merges two options of the same type. Your function runs only when both are Some; otherwise the one that exists comes back untouched.
Xor
Returns the value only if exactly one of the two is Some.
Work with a collection of them
A List<Option<T>> has its own set of helpers — Collect, Flatten, Map, Filter, FirstOrNone and friends. They live in Waystone.Monads.Options.Extensions, and are covered on the Option<T> collections reference.
To step out of a single Option and into System.Linq, use AsEnumerable. It gives you a sequence of nothing or one:
For real LINQ query syntax over an Option — from, where, select, staying inside the monad the whole way — see Waystone.Monads.Linq.
Printing and logging
ToString() never shows the wrapped value. You get the state and nothing else:
Option<T> is a record and Some<T> keeps its value in a private property, so the compiler-generated ToString() has nothing to print. Interpolating an option into a log message tells you whether a value was there, never what it was.
To log the value when it exists, use Inspect. It runs your action only on a Some, and hands the option back so the chain continues:
Nothing runs on a None. To log both branches, use Match.
When to reach for it
Use Option<T> when:
The value is intentionally optional, not missing by accident.
You want a chain that bails out early on absence.
You do not care why it is absent.
You want the caller to have to deal with the empty case.
Reach for something else when:
The default of a value type already means absence —
0for a count, say.You care about the reason. That is
Result<T, E>.
Where to go next
Result<T, E> — the same idea, for failure.
Async — keeping a chain intact across an
await.Option<T> API — every overload, when you need one this page did not show.
Last updated
Was this helpful?