Skip to content
Blog

Dart Class Modifiers: final, base, interface, and sealed

Dart 3.0's class modifiers let you control whether other libraries can extend or implement your classes. A practical guide to all four.

Published on • October 20, 2026

AI Assistant

Before Dart 3.0, a public class in Dart meant an open invitation: any library could extend it, implements it, or both. For API authors that was a liability — a third-party implements on your class breaks whenever you add a method, and a rogue subclass could override an internal method your own code depends on. Dart 3.0, released May 10, 2023, shipped class modifiers alongside patterns, records, and switch expressions to close that gap. Four keywords — final, base, interface, and sealed — let you state exactly how subtypes may be created.

Why subtyping control matters

Every Dart class implicitly declares an interface, which is what makes implements so convenient for mocking and for defining contracts. But that same mechanism means an outside library can implement your class without inherencing its invariants — reimplementing methods in ways that violate assumptions your code makes about this. Class modifiers let you choose, per type, which of the two subtyping routes (extends, implements) is open, closed, or closed to everyone but your own library.

The rules all share one boundary: restrictions apply to other libraries. Inside the library where the class is declared, everything remains possible. That’s what makes these modifiers safe to adopt incrementally — you can keep subclassing internally while freezing the public surface.

final: close the hierarchy entirely

final is the strictest option. A final class can’t be extended or implemented outside its defining library:

// in package:geometry
final class Point {
  final double x;
  final double y;
  const Point(this.x, this.y);
}

Any extends Point or implements Point in another library is a compile-time error. The guarantees: you can add methods to Point later without worrying about breaking implementers, and you can call methods on it knowing no third party has overridden them. Use final for value types and anything whose behavior you never want redefined — it’s the default recommendation when you don’t have a specific reason to allow subtyping.

base: inheritance only, and it’s enforced

base keeps extends open but closes implements:

base class Vehicle {
  void moveForward(int meters) {
    // ...a real implementation subclasses rely on
  }
}

A base class guarantees its constructor always runs for subtypes, that private members exist in every subclass, and that adding a new method later can’t collide with an outside implementation. The flip side: any class that extends a base class must itself be marked base, final, or sealed — the restriction propagates down the hierarchy so outside libraries can’t dodge it one level removed.

base also plays an important role with mixins: since Dart 3.0, classes without the mixin keyword can no longer be mixed in at all, and base mixin restricts a mixin to extension-only use.

interface: implement, don’t extend

interface is the mirror image of base: outside libraries may implements it but not extends it:

interface class Vehicle {
  void moveForward(int meters) {
    // ...
  }
}

This is designed for classes whose own methods call other instance methods on this, where letting outside subclasses override those callees would risk the fragile base class problem. Subclasses in other libraries can’t intercept those internal calls because they can’t subclass at all — but mocking frameworks and contract implementers still work. The common combination abstract interface class defines a pure contract: no instantiation, no extension, implement away.

sealed: exhaustive subtypes

sealed is the odd one out, optimized for pattern matching rather than API protection. A sealed class can’t be extended or implemented outside its library, and it’s implicitly abstract. Because the compiler knows the complete set of direct subtypes, switch expressions over it are checked for exhaustiveness at compile time:

sealed class Vehicle {}

class Car extends Vehicle {}
class Truck extends Vehicle {}
class Bicycle extends Vehicle {}

String sound(Vehicle v) => switch (v) {
      Car() => 'vroom',
      Truck() => 'VROOOOMM',
      Bicycle() => 'ring ring',
    };

Add a fourth subtype and every non-exhaustive switch in your library becomes a compile error — the compiler forces you to handle it. Note the trade-off: sealed classes freeze the subtype set just as strictly as final, so if you want to allow future subtypes from outside, use final instead and lose exhaustiveness checking.

Combining and choosing

Modifiers combine in a fixed order: optional abstract, then one of base/interface/final/sealed, then optional mixin, then class. Some combinations are contradictory — sealed is already abstract, and interface, final, or sealed conflict with mixin. So you get forms like abstract base class, abstract interface class, and mixin class.

A quick decision guide:

  • Don’t need external subtyping? → final
  • Want inheritance but not implementation? → base
  • Want implementation (mocking, contracts) but not inheritance? → interface or abstract interface
  • Want a fixed subtype set with exhaustive switches? → sealed

The feature landed in Dart 3.0 and requires a language version of at least 3.0, which any SDK constraint of ^3.0.0 or later already provides. For API maintainers retrofitting modifiers onto published packages, the Dart site has a dedicated class modifiers for API maintainers guide covering the migration strategy.

Sources