Maybe<T>
Maybe<T> is a functional type that represents an optional value: either Some value of type T, or None (the
absence of a value). It provides a type-safe alternative to null references and enables composable operations on
potentially missing values.
Table of Contents
Introduction
Use Maybe<T> when a value may legitimately be absent and that absence should be handled explicitly.
Core Concepts
When to Use
- Null Safety: Replace nullable types with explicit optionality
- API Design: Signal that a value may or may not be present (e.g., configuration values, cache lookups)
- Database Queries: Represent optional results (e.g.,
FindByIdreturningMaybe<User>) - Validation: Chain operations that may fail to produce a value
- Parsing: Handle optional or invalid input gracefully
Key Characteristics
- Zero-Allocation: Implemented as
readonly record structfor minimal overhead - Type Safety: Forces explicit handling of the None case at compile time
- Composable: Supports functional composition with
Map,Bind,Filter, etc. - LINQ Integration: Supports query syntax via
Select,SelectMany, andWhere
Simple Example
Maybe<User> GetUser(Guid id) => _cache.TryGetValue(id, out var user)
? Maybe.Some(user)
: Maybe.None<User>();
var displayName = GetUser(userId)
.Map(user => user.Name)
.ValueOr("Guest");
API Reference
The full API surface has been moved to a dedicated reference page so this guide can stay focused on concepts and practical usage.
Advanced Topics
Async Pipelines
Maybe<T> supports async pipelines with ValueTask<Maybe<T>> and Task<Maybe<T>> overloads.
ValueTask<Maybe<User>> userTask = GetUserAsync(id);
Maybe<string> name = await userTask.Map(u => u.Name);
Maybe<Profile> profile = await userTask.Bind(u => GetProfileAsync(u.ProfileId));
Best Practices
Do
- ✅ Use
Maybe<T>for optional values that genuinely may or may not exist - ✅ Chain operations with
BindandMapfor clean, composable code - ✅ Use LINQ query syntax for complex chains with multiple binds
- ✅ Prefer
ValueOrfor simple defaults andOrElsefor complex fallback logic - ✅ Convert to
Result<T>when you need to provide error context
Don't
- ❌ Use
Maybe<T>for error conditions—useResult<T>instead - ❌ Mix
Maybe<T>with nullable types unnecessarily - ❌ Use
Caseproperty directly—preferMatch,ValueOr, or pattern matching - ❌ Nest
Maybe<Maybe<T>>—useBindto flatten
Real-World Examples
Configuration Value
public Maybe<int> GetMaxRetries() =>
_configuration["MaxRetries"]
.Map(int.TryParse)
.Filter(x => x > 0);
int maxRetries = GetMaxRetries().ValueOr(3);
Database Query
public async ValueTask<Maybe<User>> FindUserByEmail(string email)
{
var user = await _dbContext.Users
.Where(u => u.Email == email)
.FirstOrDefaultAsync();
return user; // Implicit conversion from User? to Maybe<User>
}
var result = await FindUserByEmail("user@example.com")
.Map(u => new UserDto(u.Id, u.Name))
.ToResult("User not found");
Chaining Operations
var orderTotal = await GetUser(userId)
.Bind(user => GetCart(user.CartId))
.Filter(cart => cart.Items.Any())
.Map(cart => cart.Items.Sum(i => i.Price))
.TapSome(total => _logger.LogInformation("Order total: {Total}", total))
.ValueOr(0m);
LINQ Query with Multiple Sources
var profileData =
from user in GetUser(id)
from settings in GetSettings(user.SettingsId)
from avatar in GetAvatar(user.AvatarId)
where settings.IsPublic
select new PublicProfile(user.Name, avatar.Url, settings);
See Also
- Result - For operations that can fail with errors
- Failures and Metadata - For typed failure modeling and contextual metadata