Skip to content

Overview

Haitch.Unions provides Result<T> and Optional<T> union types for representing operation outcomes and optional values without exceptions or null checks. It includes a typed Error hierarchy, fluent sync and async pipelines, ASP.NET Core integration, and TUnit assertions.

Targets .NET 10+.

Terminal window
dotnet add package Haitch.Unions
dotnet add package Haitch.Unions.AspNetCore # optional: Result -> HTTP results and ProblemDetails
dotnet add package Haitch.Unions.TUnit.Assertions # optional: TUnit assertions
using Haitch.Unions;
Result<User> GetUser(int id) =>
repository.Find(id) is { } user
? user
: new NotFoundError("User.NotFound", $"User {id} not found");
var name = GetUser(42)
.Ensure(user => user.IsActive, new ValidationError("User.Inactive", "User is inactive"))
.Map(user => user.DisplayName)
.Match(
onOk: displayName => displayName,
onError: _ => "Anonymous");

Result<T> represents an operation that is either Ok, holding a T, or Error, holding an Error. There is no non-generic Result type — an operation with nothing to return uses Result<Unit>.

Optional<T> represents a value that may or may not be present: Some holding a T, or None.

Error is the base of a typed hierarchy — NotFoundError, ValidationError, ConflictError, UnauthorizedError, ForbiddenError, and UnexpectedError — instead of a single record with an enum tag.

Both Result<T> and Optional<T> are C# unions ([Union]/IUnion), so switch expressions work alongside Match — see Pattern matching.

Both Result<T> and Optional<T> support implicit conversion, so you can return values or errors directly without calling factory methods.

Result<User> GetUser(int id)
{
var user = db.Find(id);
if (user is null)
return new NotFoundError("user.not_found", $"No user with id {id}.");
return user; // implicit Ok
}
Optional<string> GetNickname(User user) => user.Nickname; // null becomes None