23  Sealed Classes in Dart

Sealed classes are one of Dart’s most powerful features for type-safe error handling. Think of them as a way to create a closed set of subtypes that the compiler knows about completely.

23.1 The Core Concept

In simple terms, a sealed class is a class that:

  1. Cannot be directly instantiated
  2. Can only be extended/implemented within the same library
  3. Forces the compiler to know all possible subtypes

Here’s a visual representation:

Regular Class Hierarchy           Sealed Class Hierarchy
=====================             ====================

    Animal                           Result (sealed)
      |                                    |
      |-- anywhere in                      |-- only in same library
      |   your codebase                    |
      |                                    |
  ┌───┴───┬────┬────┐               ┌──────┴──────┐
  |       |    |    |               |             |
 Dog    Cat  Bird  ?              Success      Failure
                   ?                            
 (can add more)                  (complete set!)

23.2 Basic Example

Let me show you a practical example - handling API responses:

// Define the sealed class
sealed class Result<T> {
  const Result();
}

// All subtypes must be in the same file
final class Success<T> extends Result<T> {
  final T data;
  const Success(this.data);
}

final class Failure<T> extends Result<T> {
  final String error;
  const Failure(this.error);
}

23.3 The Magic: Exhaustive Pattern Matching

Now here’s where sealed classes shine. When you use pattern matching with a sealed class, the compiler forces you to handle ALL cases:

String handleResult(Result<String> result) {
  return switch (result) {
    Success(data: var d) => 'Got data: $d',
    Failure(error: var e) => 'Error: $e',
    // If you forget a case, you get a COMPILE-TIME error!
  };
}

If you try to omit one of the cases:

String handleResult(Result<String> result) {
  return switch (result) {
    Success(data: var d) => 'Got data: $d',
    // Compiler error: "The type 'Result<String>' is not exhaustively matched"
  };
}

23.4 Compare with Regular Classes

With a regular abstract class, this wouldn’t work:

// Regular abstract class
abstract class ApiResponse {
  const ApiResponse();
}

class Loading extends ApiResponse {}
class Success extends ApiResponse {}
class Error extends ApiResponse {}

// Someone in another file could add:
class SomeOtherCase extends ApiResponse {}

// The compiler can't enforce exhaustiveness!
String handle(ApiResponse response) {
  return switch (response) {
    Loading() => 'loading',
    Success() => 'success',
    // Compiler doesn't know about all possible subtypes
  };
}

23.5 Real-World Pattern: State Management

Here’s a practical example you might use in Flutter - representing UI states:

sealed class LoadingState<T> {
  const LoadingState();
}

final class Initial<T> extends LoadingState<T> {
  const Initial();
}

final class Loading<T> extends LoadingState<T> {
  const Loading();
}

final class Loaded<T> extends LoadingState<T> {
  final T data;
  const Loaded(this.data);
}

final class LoadError<T> extends LoadingState<T> {
  final String message;
  final Exception? exception;
  const LoadError(this.message, [this.exception]);
}

// In your Flutter widget:
Widget buildContent(LoadingState<List<Patient>> state) {
  return switch (state) {
    Initial() => Text('Tap to load patients'),
    Loading() => CircularProgressIndicator(),
    Loaded(data: var patients) => PatientList(patients),
    LoadError(message: var msg) => ErrorWidget(msg),
    // Must handle all cases - compiler enforces this!
  };
}

23.6 Connection to Your Python Experience

If you’ve used Python’s typing with Union types and match statements (Python 3.10+), sealed classes are similar but compiler-enforced:

# Python (not enforced at compile time)
from typing import Union
from dataclasses import dataclass

@dataclass
class Success:
    data: str

@dataclass
class Failure:
    error: str

Result = Union[Success, Failure]

# mypy can check this, but not enforced at runtime
def handle(result: Result) -> str:
    match result:
        case Success(data):
            return f"Got: {data}"
        # If you forget Failure, mypy warns but Python runs

Dart’s sealed classes give you this pattern with compile-time guarantees.

23.7 Key Benefits for Your Radiology AI Work

  1. API Response Handling: Model different response states type-safely
  2. Image Processing States: Represent processing pipeline states
  3. Validation Results: Handle validation with all edge cases covered
  4. Error Types: Create domain-specific error hierarchies

Example for medical imaging:

sealed class DicomLoadResult {
  const DicomLoadResult();
}

final class DicomLoaded extends DicomLoadResult {
  final DicomImage image;
  final DicomMetadata metadata;
  const DicomLoaded(this.image, this.metadata);
}

final class DicomCorrupted extends DicomLoadResult {
  final String reason;
  const DicomCorrupted(this.reason);
}

final class DicomNotFound extends DicomLoadResult {
  final String path;
  const DicomNotFound(this.path);
}

final class DicomUnauthorized extends DicomLoadResult {
  const DicomUnauthorized();
}

Now when you handle DICOM loading, the compiler forces you to think about all error cases - no forgotten edge cases that could crash your radiology app!

23.8 Examples

23.8.1 Result Type

File: lib/core/result.dart (NEW)

Implementation:

 /// Represents the result of an operation that can succeed or fail
 sealed class Result<T, E> {
   const Result();
 }

 /// Success case containing the result value
 final class Success<T, E> extends Result<T, E> {
   final T value;
   const Success(this.value);
 }

 /// Failure case containing the error
 final class Failure<T, E> extends Result<T, E> {
   final E error;
   const Failure(this.error);
 }

 // Extension methods for convenience
 extension ResultExtensions<T, E> on Result<T, E> {
   bool get isSuccess => this is Success<T, E>;
   bool get isFailure => this is Failure<T, E>;

   T? get valueOrNull => switch (this) {
     Success(value: final v) => v,
     Failure() => null,
   };

   E? get errorOrNull => switch (this) {
     Success() => null,
     Failure(error: final e) => e,
   };
 }

23.8.2 Calculator Error Types

File: lib/services/calculator/calculator_error.dart (NEW)

Error Hierarchy:

/// Base sealed class for calculator errors
sealed class CalculatorError {
  final String message;
  const CalculatorError(this.message);
}

/// Input parsing failed (non-numeric, invalid format)
final class ParseError extends CalculatorError {
  final String fieldName;
  final String input;

  ParseError(this.fieldName, this.input)
    : super('$fieldName must be a valid number');
}

/// Business rule validation failed
final class ValidationError extends CalculatorError {
  ValidationError(String message) : super(message);
}

/// Mathematical operation error
final class CalculationError extends CalculatorError {
  CalculationError(String message) : super(message);
}