Skip to content

Beginner · Null safety and object-oriented Dart · topic 8 of 50 · 35 min

Dart 3: enums, records, patterns and sealed classes

Explanation

Enhanced enums can have fields, constructors and methods. Records group a few values without a class, like (String, int) or ({int lat, int lng}). Patterns destructure values in switch, if-case and variable declarations. A sealed class has a fixed set of subclasses in its library, so a switch over it is checked for exhaustiveness: perfect for UI states like Loading, Success and Error.

Enhanced enums

An enum is a fixed set of named values. Since Dart 2.17, enums can carry fields and methods, which removes a lot of switch statements scattered around an app.

dart
enum PaymentMode {
  upi('UPI', isOnline: true),
  card('Card', isOnline: true),
  cash('Cash on delivery', isOnline: false);

  const PaymentMode(this.label, {required this.isOnline});
  final String label;
  final bool isOnline;
}

void main() {
  for (final m in PaymentMode.values) {
    print('${m.label}: ${m.isOnline ? 'online' : 'offline'}');
  }
}
values lists all the constants; each constant has its own label.

Records

Sometimes you want to return two or three values from a function without creating a class. A record is a small, immutable group of values. It can have positional fields (accessed as $1, $2) and named fields.

dart
(int, int) splitBill(int total, int people) => (total ~/ people, total % people);

({double lat, double lng}) shopLocation() => (lat: 18.52, lng: 73.85);

void main() {
  final (each, left) = splitBill(1000, 3);     // destructuring a record
  print('Each pays $each, $left left over');   // Each pays 333, 1 left over

  final loc = shopLocation();
  print('${loc.lat}, ${loc.lng}');
}
Records compare by value: (1, 2) == (1, 2) is true.

Patterns

A pattern describes the shape of a value and can pull parts out of it. Dart 3 uses patterns in switch, in if-case, in variable declarations and in for-in loops.

dart
String describe(Object value) => switch (value) {
      int n when n < 0 => 'negative number',
      int n => 'number $n',
      String s when s.isEmpty => 'empty text',
      String s => 'text of length ${s.length}',
      (int a, int b) => 'pair $a and $b',
      _ => 'something else',
    };

void main() {
  final json = {'name': 'Asha', 'age': 21};
  if (json case {'name': String name, 'age': int age}) {
    print('$name is $age');          // Asha is 21
  }
  print(describe((3, 4)));           // pair 3 and 4
}
when adds a guard condition. if-case checks the shape of JSON safely and binds the values.

Patterns replace many lines of manual checks. Without them you would write "if the value is a Map, and it has a key name, and that value is a String..." step by step. With a pattern, you describe the shape you expect once, and Dart checks every part and binds the variables only if everything matches.

Sealed classes

A sealed class can only be extended or implemented inside its own library (file). The compiler therefore knows every subtype, and a switch over a sealed type must handle all of them, without a default case. This is the standard way to model screen states and results.

dart
sealed class LoadState {}
class Loading extends LoadState {}
class Loaded extends LoadState {
  final List<String> items;
  Loaded(this.items);
}
class Failed extends LoadState {
  final String message;
  Failed(this.message);
}

String render(LoadState s) => switch (s) {
      Loading() => 'Showing spinner',
      Loaded(:final items) => 'Showing ${items.length} items',
      Failed(:final message) => 'Error: $message',
    };   // no default needed; add a new subtype and this stops compiling
Loaded(:final items) is an object pattern that binds the items field.

Class modifiers in one table

ModifierMeaning (outside the library)
sealedCannot be extended or implemented; enables exhaustive switch
finalCannot be extended or implemented
baseCan be extended, not implemented
interfaceCan be implemented, not extended
mixin classCan be used both as a class and as a mixin
Think of it like this: A sealed class is like a railway ticket that can only be one of a printed list of classes: SL, 3A, 2A, 1A. Because the list is fixed and printed, the ticket checker's chart covers every case; nothing unexpected can appear.

Common mistakes

  • Adding a default _ case to a switch over a sealed type: it hides newly added subtypes. Leave it out.
  • Using records for data that travels across the whole app. For long-lived models, a named class with documentation is clearer.
  • Writing JSON access like json['age'] as int without checking. if-case patterns check shape and type together.
  • Forgetting that sealed subclasses must be in the same library (file) as the sealed class.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Dart 3: enums, records, patterns and sealed classes · 1/6

Enums | Records | Patterns | Sealed

Dart three added powerful features for modelling data: enhanced enums, records, patterns and sealed classes.

Prefer a video by a teacher? Find free videos on “dart 3 records patterns sealed classes enhanced enums switch expressions” on YouTube ↗

In real life: Payment results in a checkout flow

A checkout screen can end in three ways: success with a transaction reference, failure with a reason, or "pending" when the bank has not confirmed. A sealed PaymentResult with three subclasses makes the result screen's switch exhaustive. When the team later adds "refunded", the compiler lists every screen that must handle it.

dart
sealed class PaymentResult {}
class Paid extends PaymentResult { final String utr; Paid(this.utr); }
class Declined extends PaymentResult { final String reason; Declined(this.reason); }
class Pending extends PaymentResult {}

String message(PaymentResult r) => switch (r) {
      Paid(:final utr) => 'Paid. Reference $utr',
      Declined(:final reason) => 'Payment failed: $reason',
      Pending() => 'Waiting for bank confirmation',
    };

void main() => print(message(Declined('insufficient balance')));
Prints: Payment failed: insufficient balance

Practice (0/3 done)

Task 1 · Question

What does this print? (int, String) info() => (7, 'seven'); void main() { final (n, word) = info(); print('$word=$n'); }

Task 2 · Question

Why should a switch over a sealed class usually have no default case?

Task 3 · Practice

Model a delivery tracker: an enhanced enum Vehicle (bike, van) with a speedKmph field; a sealed class DeliveryState with Placed, OnTheWay(rider, minutes) and Delivered(time); and a function that returns a message using a switch with object patterns. Also return (eta, distance) as a record from a function and destructure it.

Quick quiz

Question 1

1.What is a record in Dart?

Question 2

2.Where can subclasses of a sealed class be declared?

Question 3

3.What does when do in a switch pattern?

Question 4

4.Which feature lets an enum value carry a label and a flag?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is a record in Dart?

Answer: A small immutable group of values, like (String, int)

Records bundle values without a class.

Where can subclasses of a sealed class be declared?

Answer: Only in the same library (file)

That is what makes the set known.

What does when do in a switch pattern?

Answer: Adds a guard condition to a case

The case matches only if the guard is true.

Which feature lets an enum value carry a label and a flag?

Answer: Enhanced enums with fields

Enums can have fields and constructors.

Why can a when over a sealed type skip else?Related

Answer: The compiler knows every subtype and checks all are handled

Exhaustiveness is checked at compile time.

From Data, enum and sealed classes, and objects

Which Dart 3 feature makes a switch over UI states exhaustive?Related

Answer: Sealed classes

All subtypes are known.

From Interview questions: Dart

What does sealed interface Payment permits Upi, Card {} do?Related

Answer: Allows only Upi and Card to implement Payment

Sealed types restrict their implementations.

From Interfaces

String s = null; switch (s) { case "a" -> … default -> … } gives…Related

Answer: NullPointerException

Without case null, switching on null throws NPE.

From switch statements and switch expressions

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Null safety and object-oriented Dart”. The module exam comes next.
  2. Module 2 exam: Null safety and object-oriented Dart · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Async Dart and packages. 50 topics left in the course before the final exam.

← Previous: Inheritance, interfaces, mixins and extensions