Dart/Flutter - Sealed Classes Examples

This tutorial explains what is a sealed class in Dart programming language and the examples how to use it.

Dart 3 introduced a new modifier called sealed. It can be used to define a type or class whose list of subtypes is restricted. Below are the explanations and examples of how to use it.

Sealed Classes

For example, we want to have a class named Animal. There can only be three classes that can be the subtype of the Animal class: Cow, Cat, and Goat.

The solution is by adding a sealed modifier to the Animal class. With that modifier, the class cannot be extended or implemented outside its own library. In addition, sealed classes are implicitly abstract.

Creating Subclasses

All subclasses must be declared in the same library (the same file). Below is the example of how to declare the Animal class along with its subclasses.

  sealed class Animal {}
  
  class Cow extends Animal {}
  
  class Cat extends Animal {}
  
  class Goat extends Animal {
    final String name;
  
    Goat(this.name);
  }

If you try to define a class that extends the Animal class in another file, the compiler will give you an error.

  // Error: Classes can only extend other classes.
  // Try specifying a different superclass, or removing the extends clause.
  class Tapir extends Animal {} 

Creating Instances

Because a sealed class is implicitly abstract, you cannot create an instance using its constructor.

  // Error: Abstract classes can't be instantiated.
  Animal animal = Animal();

The subclasses of a sealed class are not implicitly abstract. Therefore, you have to use the subclasses' constructor for creating the instances.

  Cow myCow = Cow();
  Cat myCat = Cat();
  Goat myGoat = Goat('woolha.com');

Creating Constructors

A sealed class can define constructors which can be called by its subclasses. For example, we modify the Animal class above to have a field named id. There's a constructor that accepts the id value. To make it easier to check whether the constructor is invoked when from the subclasses, I added a print statement.

  sealed class Animal {
  
    String id;
  
    Animal(this.id) {
      print('Creating an animal');
    }
  }
  
  class Cow extends Animal {
    Cow(super.id);
  }
  
  class Cat extends Animal {
    Cat(super.id);
  }
  
  class Goat extends Animal {
    final String name;
  
    Goat(String id, this.name) : super(id) {
      print('Creating a goat');
    }
  }

With the above code, when you try to create an instance of the subclass, you should be able to see that the 'Creating an animal' text is printed.

It's also possible to define some factory constructors.

  sealed class Animal {
  
    String id;
  
    Animal(this.id) {
      print('Creating an animal');
    }
  
    factory Animal.createByType(String type, String id) {
      if (type == "cow") {
        return Cow(id);
      } else if (type == "cat") {
        return Cat(id);
      } else if (type == "goat") {
        return Goat(id, 'woolha.com');
      }
      
      throw UnsupportedError('Unknown type');
    }
  }

Switch Cases Expression Checking

In Dart, you can create a switch case to check whether a given object is an instance of a particular class. Because a sealed class has a known list of subtypes, the compiler is able to give you an alert if a switch block doesn't handle all possible subtypes.

In the following example, the switch block doesn't handle the case if the passed object is a Goat. As a result, it will give an error that the type is not exhaustively matched by the switch cases.

  String getAnimalVoice(Animal animal) {
    // ERROR: The type 'Animal' is not exhaustively matched by the switch cases since it doesn't match   'Goat()'
    return switch (animal) {
      Cow() => 'moo',
      Cat() => 'meow',
    };
  }

Summary

Dart's sealed modifier is suitable if you want to define a class whose list of subtypes are known from the beginning and cannot be changed later. The list of subtypes must be declared in the same library (file). Sealed classes are implicitly abstract and cannot be instantiated. However, you can add some constructors, including factory constructors. If you create a switch block where the checked object type is a sealed class, the compiler can check whether the type is already exhaustively matched by the switch cases.