DEV Community

Finite Field
Finite Field

Posted on

Dart's Sealed Classes: A Powerful Tool for Type Safety and Exhaustiveness

Dart, like many modern languages, is continually evolving to provide developers with better tools for writing robust and maintainable code. One feature that significantly enhances type safety and code clarity is the sealed class modifier. Introduced in Dart 3.0, sealed classes offer a way to restrict the inheritance hierarchy of a class, enabling powerful patterns and catching potential errors at compile time.

In this post, we'll delve into what sealed classes are, how they work, and why you should consider using them in your Dart projects.

What are Sealed Classes?

At their core, sealed classes are a mechanism for defining a class that can only be inherited by a specific, predefined set of subclasses. In essence, a sealed class creates a closed hierarchy. This is a powerful contrast to regular classes, which can be extended by any other class in your application.

Think of it like this: a regular class is a door that anyone can enter, while a sealed class is a door with a very specific list of allowed visitors.

Key characteristics of sealed classes:

  • Restricted Inheritance: Only the classes declared within the same library as the sealed class can extend or implement it.
  • Compile-Time Exhaustiveness: When working with a sealed class, the Dart compiler can enforce that your code handles all possible subclasses. This leads to more robust code and prevents potential runtime errors.
  • No Implementation: A sealed class does not have to have any implementation. It is simply a marker class.

Syntax of Sealed Classes

Declaring a sealed class is straightforward. You simply add the sealed keyword before the class keyword:

sealed class Shape {}
Enter fullscreen mode Exit fullscreen mode

Now, let's define some allowed subclasses within the same library (usually in the same file):

sealed class Shape {}

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

class Square extends Shape {
  final double side;
  Square(this.side);
}

class Triangle extends Shape {
  final double base;
  final double height;
  Triangle(this.base, this.height);
}

// This won't compile because it's in a different file
//class Rectangle extends Shape {}
Enter fullscreen mode Exit fullscreen mode

In this example, Shape is our sealed class, and Circle, Square, and Triangle are the only classes that can inherit from it. If you try to create another class in a different file that extends Shape, you'll get a compile-time error.

The Power of Exhaustiveness

The real magic of sealed classes lies in the compile-time exhaustiveness check. Dart can use the information about the limited number of subclasses of a sealed class to ensure that your switch statements or if/else chains cover all the possible cases.

Here's how that looks:

sealed class Shape {}

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

class Square extends Shape {
  final double side;
  Square(this.side);
}

class Triangle extends Shape {
  final double base;
  final double height;
  Triangle(this.base, this.height);
}

double calculateArea(Shape shape) {
  switch (shape) {
    case Circle():
      return 3.14 * shape.radius * shape.radius;
    case Square():
      return shape.side * shape.side;
    case Triangle():
      return 0.5 * shape.base * shape.height;
  }
}

Enter fullscreen mode Exit fullscreen mode

In the calculateArea function, if you were to forget one of the cases (e.g., the Triangle case), the Dart compiler would throw an error. This immediately points out that your code is incomplete and helps prevent potential runtime bugs. This is why sealed classes are particularly useful in combination with pattern matching.

With regular classes, if you add a new subclass, your old code may break silently at runtime. The compiler will not give a warning because it doesn't know that the subclasses are closed. But, in the case of sealed classes, the compiler gives a compile-time error forcing you to address it immediately, leading to more robust and maintainable code.

Use Cases for Sealed Classes

Sealed classes are a valuable tool in several scenarios:

  1. State Management: When managing application state, you often have a distinct set of states that an object can be in. Using a sealed class to represent the possible states ensures your code handles them all correctly. This is particularly helpful when you are dealing with asynchronous operations. For example:

    sealed class DataState {}
    class Loading extends DataState{}
    class Loaded extends DataState{
      final dynamic data;
      Loaded(this.data);
    }
    class Error extends DataState {
      final String errorMessage;
      Error(this.errorMessage);
    }
    
  2. Algebraic Data Types (ADTs): Sealed classes excel at modeling ADTs, where data can take on a finite set of forms. This enables you to express your domain logic very clearly and have the compiler enforce that your code correctly handles all the possible cases.

  3. Representing Events: In event-driven architectures, sealed classes can represent different event types. This allows you to build event handlers that are guaranteed to cover all possible events.

  4. Exhaustive switch cases: The compiler enforces that you have handled all the possible type with in switch statement.

Benefits of Using Sealed Classes

  • Enhanced Type Safety: Compile-time checks help you catch missing cases, reducing runtime errors.
  • Improved Code Maintainability: The compiler forces you to revisit and update the relevant code when new subclasses are added, preventing your application from breaking silently.
  • Clearer Domain Modeling: Sealed classes help you precisely express the structure of your data, making it more understandable.

Conclusion

The sealed class modifier is a significant addition to Dart, bringing compile-time exhaustiveness and improved type safety. They're a valuable tool to add to your development arsenal, particularly when dealing with state, ADTs, events, or any situation where you have a well-defined set of types that you need to handle exhaustively.

By adopting sealed classes, you can write more robust, maintainable, and less error-prone Dart code. So, next time you're working on a Flutter or Dart project, consider if a sealed class could help streamline your logic. At Finite Field, we understand the power of cutting-edge features like sealed classes. As a dedicated app development company, we leverage these kinds of tools, along with deep expertise in Flutter and Dart, to craft innovative and high-quality mobile applications for our clients. If you're looking for a partner to bring your app idea to life with a focus on maintainable and scalable code, we'd love to hear from you.

Top comments (0)