Skip to content
AgileLogics
6 min read

Sealed Classes and Exhaustive Switching

Prerequisites: Patterns and destructuring, Enums

Enums work when every option is a single value. When each option needs to carry different data, use a sealed class: a closed family of subclasses that the compiler knows completely.

Example: modeling results

sealed class PaymentResult {}

class Success extends PaymentResult {
  final String transactionId;
  Success(this.transactionId);
}

class Declined extends PaymentResult {
  final String reason;
  Declined(this.reason);
}

class Pending extends PaymentResult {}

String message(PaymentResult result) => switch (result) {
      Success(:final transactionId) => 'Paid (ref $transactionId)',
      Declined(:final reason) => 'Declined: $reason',
      Pending() => 'Waiting for the bank...',
    };

void main() {
  print(message(Success('TX-1042')));
  print(message(Declined('insufficient funds')));
  print(message(Pending()));
}

Expected output

Paid (ref TX-1042)
Declined: insufficient funds
Waiting for the bank...
  • A sealed class can't be created directly, and it can only be subclassed in the same file — so the compiler knows every subtype.
  • That makes the switch exhaustive with no _ case. Add a new subtype, and every switch that doesn't handle it becomes a compile error — exactly where your attention is needed.
  • Success(:final transactionId) matches the type and reads the field.

Making impossible states impossible

Compare screen state modeled with separate nullable fields — where data and error could both be set by mistake — with a sealed type:

sealed class ScreenState {}

class Loading extends ScreenState {}

class Loaded extends ScreenState {
  final List<String> items;
  Loaded(this.items);
}

class Failed extends ScreenState {
  final String message;
  Failed(this.message);
}

String render(ScreenState state) => switch (state) {
      Loading() => 'Spinner',
      Loaded(:final items) when items.isEmpty => 'Nothing here yet',
      Loaded(:final items) => 'Showing ${items.length} items',
      Failed(:final message) => 'Error: $message',
    };

void main() {
  print(render(Loading()));
  print(render(Loaded([])));
  print(render(Loaded(['Ada', 'Linus'])));
  print(render(Failed('offline')));
}
Spinner
Nothing here yet
Showing 2 items
Error: offline

This pattern is common in Flutter apps: a screen's state is exactly one of these cases, and the UI switches on it.

Practice

Define sealed class Shape with Circle(radius) and Rectangle(width, height) subclasses, and an area function using a switch expression. Print both areas with two decimal places for a circle of radius 1 and a 2 by 3 rectangle.

Show solution
import 'dart:math';

sealed class Shape {}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);
}

class Rectangle extends Shape {
  final double width, height;
  Rectangle(this.width, this.height);
}

double area(Shape shape) => switch (shape) {
      Circle(:final radius) => pi * radius * radius,
      Rectangle(:final width, :final height) => width * height,
    };

void main() {
  print(area(Circle(1)).toStringAsFixed(2));
  print(area(Rectangle(2, 3)).toStringAsFixed(2));
}
3.14
6.00