Option<T>
Option<T> represents a value that may or may not be present. It's a type-safe alternative to null that makes the absence of a value explicit in your type signatures.
Creating Options
// Explicit construction
var some = Option.Some(42); // Some(42)
var none = Option.None<int>(); // None
// From nullable references
string? name = GetName();
var option = Option.From(name); // Some or None
// From nullable value types
int? age = GetAge();
var option = Option.From(age); // Some or None
// Implicit conversion
Option<int> x = 42; // Some(42)
Try and TryParse
Safely wrap operations that might throw:
// Catch exceptions and return None
var parsed = Option.Try(() => int.Parse("not a number")); // None
var valid = Option.Try(() => int.Parse("42")); // Some(42)
// Async version
var data = await Option.TryAsync(() => FetchDataAsync());
// Type-safe parsing via IParsable<T>
var number = Option.TryParse<int>("42"); // Some(42)
var bad = Option.TryParse<int>("abc"); // None
var date = Option.TryParse<DateOnly>("2024-01-15"); // Some(2024-01-15)
Transforming Values
Map
Transform the value inside an Option. If the Option is None, the function is not called:
var greeting = Option.Some("Alice")
.Map(name => $"Hello, {name}!"); // Some("Hello, Alice!")
var nothing = Option.None<string>()
.Map(name => $"Hello, {name}!"); // None
Bind
Chain operations that themselves return Options (flatMap):
Option<User> FindUser(int id) => /* ... */;
Option<Address> GetAddress(User user) => /* ... */;
var address = FindUser(123)
.Bind(user => GetAddress(user)); // Some(address) or None
Filter
Keep the value only if a predicate is satisfied:
var adult = Option.Some(25).Filter(age => age >= 18); // Some(25)
var minor = Option.Some(15).Filter(age => age >= 18); // None
Extracting Values
Match (recommended)
Exhaustively handle both cases:
var message = option.Match(
some: value => $"Found: {value}",
none: () => "Not found");
GetValueOrDefault
Provide a fallback:
var value = option.GetValueOrDefault(0);
var value = option.GetValueOrDefault(() => ComputeDefault());
GetValueOrThrow
Escape hatch — throws InvalidOperationException if None:
var value = option.GetValueOrThrow(); // throws if None
OrElse
Provide an alternative Option:
var result = primaryLookup.OrElse(fallbackLookup);
var result = primaryLookup.OrElse(() => ExpensiveFallback());
Side Effects
option
.Tap(value => Console.WriteLine($"Got: {value}"))
.TapNone(() => Console.WriteLine("Nothing found"));
LINQ Support
var result =
from user in FindUser(123)
from address in GetAddress(user)
where address.City == "London"
select address.PostCode;
IEnumerable Support
Option<T> implements IEnumerable<T> — Some yields one element, None yields zero:
var values = options.SelectMany(opt => opt); // flatten to present values
Async Operations
Every operation has an async variant:
var result = await option
.MapAsync(async x => await TransformAsync(x))
.BindAsync(async x => await LookupAsync(x))
.MatchAsync(
some: async x => await FormatAsync(x),
none: () => Task.FromResult("Not found"));