Lesson 12 / 25

Enhanced Enums and Dart 3 Class Modifiers

Use enhanced enums and the sealed, final, base and interface modifiers.

Controlling how types can be used

Enhanced enums (Dart 2.17+) can have fields, constructors, methods and implement interfaces: enum OrderStatus { placed('Placed'), paid('Paid'); const OrderStatus(this.label); final String label; }. Enums also give you values, name and byName. Dart 3 class modifiers let library authors control how classes are used outside their library. sealed declares a closed family: subclasses must be in the same library, so a switch over a sealed type can be checked for exhaustiveness, which is perfect for modelling results and states. final prevents extending and implementing outside the library. base allows extending but not implementing outside the library, guaranteeing implementation inheritance. interface allows implementing but not extending outside the library. mixin class declares something usable as both a mixin and a class. These modifiers matter most for package authors, but sealed is useful in every app for modelling state.

An enhanced enum and a sealed result type

Exhaustive switching over sealed subclasses.

enum OrderStatus {
  placed('Placed'),
  paid('Paid'),
  shipped('On the way'),
  cancelled('Cancelled');

  const OrderStatus(this.label);
  final String label;

  bool get isFinal => this == shipped || this == cancelled;
}

sealed class PaymentResult {}

class PaymentSuccess extends PaymentResult {
  PaymentSuccess(this.paymentId);
  final String paymentId;
}

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

class PaymentPending extends PaymentResult {}

String describe(PaymentResult r) => switch (r) {      // exhaustive: compiler checks all subtypes
      PaymentSuccess(:final paymentId) => 'Paid ($paymentId)',
      PaymentDeclined(:final reason) => 'Declined: $reason',
      PaymentPending() => 'Waiting for confirmation',
    };

void main() {
  print(OrderStatus.paid.label);                   // Paid
  print(OrderStatus.values.byName('shipped').isFinal);   // true
  print(describe(PaymentDeclined('Insufficient funds')));
}

Sealed types make new cases loud

Adding a new subclass to a sealed family turns every non-exhaustive switch into a compile error, pointing you to each place that must handle the new case.

Quick check: What does marking a class sealed enable?

  • Exhaustiveness checking in switch, because all subtypes are known within the library
  • Faster method calls
  • Multiple inheritance
  • Automatic JSON serialisation
Answer

Exhaustiveness checking in switch, because all subtypes are known within the library — Sealed classes have a known, closed set of subtypes.