Skip to main content

Basic Concepts

Understanding the core concepts behind UnionGenerator will help you write better, safer code. Let's explore discriminated unions, pattern matching, and how they work together.

What is a Discriminated Union?​

A discriminated union (also called a tagged union or sum type) is a type that can be one of several predefined variants, where each variant has:

  1. A tag (discriminator) that identifies which variant it is
  2. Associated data specific to that variant
  3. Type safety that ensures you handle all cases

Think of it like a type-safe "one of these" container:

// Without unions: multiple return types with nullable hell
public class ApiResponse
{
public User? User { get; set; }
public string? Error { get; set; }
public bool IsSuccess { get; set; }
}

// With unions: clear, type-safe alternatives
[GenerateUnion]
public partial record ApiResponse
{
public static partial ApiResponse Success(User user);
public static partial ApiResponse Error(string message);
}

The Power of Pattern Matching​

Pattern matching lets you safely extract values from a union based on its current variant. UnionGenerator provides multiple ways to match:

Switch Expression Pattern​

The most elegant and type-safe approach:

var result = apiResponse.Match(
success => $"Welcome, {success.User.Name}!",
error => $"Failed: {error.Message}"
);

Traditional Switch Statement​

For when you need more complex logic:

switch (apiResponse)
{
case ApiResponse.Success success:
Console.WriteLine($"User: {success.User.Name}");
SaveToDatabase(success.User);
break;
case ApiResponse.Error error:
Console.WriteLine($"Error: {error.Message}");
LogError(error.Message);
break;
}

TryGet Pattern​

When you only care about one specific variant:

if (apiResponse.TryGetSuccess(out var success))
{
Console.WriteLine($"Got user: {success.User.Name}");
}

Union Anatomy​

Let's break down what UnionGenerator creates for you:

[GenerateUnion]
public partial record PaymentResult
{
public static partial PaymentResult Completed(string transactionId, decimal amount);
public static partial PaymentResult Pending(string orderId);
public static partial PaymentResult Failed(string reason, PaymentError error);
}

Generated Components​

UnionGenerator creates several components behind the scenes:

1. Case Classes​

Each variant becomes a nested record:

// Generated by UnionGenerator
public partial record PaymentResult
{
public sealed record Completed(string TransactionId, decimal Amount) : PaymentResult;
public sealed record Pending(string OrderId) : PaymentResult;
public sealed record Failed(string Reason, PaymentError Error) : PaymentResult;
}

2. Factory Methods​

Type-safe constructors for each variant:

var completed = PaymentResult.Completed("TX-12345", 99.99m);
var pending = PaymentResult.Pending("ORD-67890");
var failed = PaymentResult.Failed("Insufficient funds", PaymentError.InsufficientFunds);

3. Match Methods​

Exhaustive pattern matching support:

// Match with return value
var message = payment.Match(
completed => $"Payment {completed.TransactionId} completed: ${completed.Amount}",
pending => $"Order {pending.OrderId} is pending",
failed => $"Payment failed: {failed.Reason}"
);

// Match with side effects
payment.Match(
completed => SendReceipt(completed.TransactionId),
pending => SendPendingNotification(pending.OrderId),
failed => LogPaymentError(failed.Reason, failed.Error)
);

4. TryGet Methods​

Safe variant extraction:

if (payment.TryGetCompleted(out var completed))
{
Console.WriteLine($"Transaction ID: {completed.TransactionId}");
Console.WriteLine($"Amount: ${completed.Amount}");
}

5. Is Properties​

Quick variant checks:

if (payment.IsCompleted)
{
// Handle completed payment
}

Type Safety Guarantees​

UnionGenerator provides compile-time safety in several ways:

Exhaustive Matching​

The compiler ensures you handle all cases:

var result = payment.Match(
completed => "Completed",
pending => "Pending"
// ERROR: Missing case for 'Failed'
);

No Null References​

Union variants never return null - you always get a valid variant:

// No null checks needed!
var payment = PaymentResult.Completed("TX-123", 50.00m);
// payment is guaranteed to be non-null

Invalid State Prevention​

Impossible states become unrepresentable:

// Before: All these states are possible (even invalid ones)
public class PaymentState
{
public bool IsCompleted { get; set; }
public bool IsPending { get; set; }
public bool IsFailed { get; set; }
// What if IsCompleted AND IsFailed are both true? 🤔
}

// After: Only valid states are possible
[GenerateUnion]
public partial record PaymentState
{
public static partial PaymentState Completed();
public static partial PaymentState Pending();
public static partial PaymentState Failed();
// Can only be ONE of these at a time! ✅
}

Common Design Patterns​

Result Pattern​

Represent success or failure:

[GenerateUnion]
public partial record Result<T, TError>
{
public static partial Result<T, TError> Success(T value);
public static partial Result<T, TError> Failure(TError error);
}

// Usage
public Result<User, string> GetUser(int id)
{
var user = database.FindUser(id);
return user != null
? Result<User, string>.Success(user)
: Result<User, string>.Failure("User not found");
}

Option Pattern​

Represent presence or absence:

[GenerateUnion]
public partial record Option<T>
{
public static partial Option<T> Some(T value);
public static partial Option<T> None();
}

// Usage
public Option<User> FindUserByEmail(string email)
{
var user = users.FirstOrDefault(u => u.Email == email);
return user != null
? Option<User>.Some(user)
: Option<User>.None();
}

State Machine Pattern​

Model complex state transitions:

[GenerateUnion]
public partial record OrderState
{
public static partial OrderState Draft(List<OrderItem> items);
public static partial OrderState Submitted(DateTime submittedAt);
public static partial OrderState Processing(string workerId);
public static partial OrderState Shipped(string trackingNumber);
public static partial OrderState Delivered(DateTime deliveredAt);
public static partial OrderState Cancelled(string reason);
}

Next Steps​

Now that you understand the core concepts, you're ready to:

Key Takeaways​

✅ Discriminated unions provide type-safe alternatives to nullable types and boolean flags
✅ Pattern matching ensures you handle all cases exhaustively
✅ UnionGenerator creates all the boilerplate automatically
✅ Compile-time safety prevents invalid states and missing cases
✅ Common patterns like Result and Option are easy to implement