Dart 27 🎯 Class Modifiers (Dart 3+): base, interface, final, sealed
A class in Dart is open by default. Any other library can extend it, implement it, or mix it in. This openness is convenient, but it is also a liability. A library author who adds a method to a class cannot know whether a downstream implementer has already defined a member with that name. A class that relies on private state cannot trust that an external implementer provides it. A type that represents a closed set of states cannot be pattern-matched exhaustively if external code can add new subtypes. Dart 3 introduced class modifiers to give library authors precise control over these questions.
This chapter covers the four class modifiers: base, interface, final, and sealed. You will learn what each one restricts, how they interact with abstract, mixin, and each other, and the design situations each one addresses. You will also see the compiler errors that enforce the restrictions and the migration considerations for code that was written before these modifiers existed.
Key point: A class modifier sits before the class keyword and constrains how the class can be used outside its own library. base allows extension but not implementation. interface allows implementation but not extension. final allows neither. sealed allows neither and additionally enables exhaustive pattern matching. The modifiers are enforced at compile time across library boundaries, and subclasses must repeat the modifier.
Why class modifiers exist
The fragile base class problem. A class that is designed to be extended exposes its implementation as part of its contract. If a subclass overrides a method that the superclass’s other methods depend on, the superclass can break in ways it cannot control. A SortedList that calls its own add method internally can be broken by a subclass that overrides add to do something else. The interface modifier prevents this by disallowing extension from outside the library, while still allowing the contract to be implemented.
The implementation inheritance problem. A class that uses private members internally cannot be safely implemented by external code. The private members are part of the class’s behavior but not part of its interface. An external implementer provides the public members but not the private ones, and the class’s own methods, if called on the implementer, fail at runtime. The base modifier prevents this by requiring that external subtypes inherit the implementation, including the private members.
The exhaustive matching problem. An enum is a closed set of values, and a switch over an enum can be checked for exhaustiveness. A class hierarchy is not closed by default—any library can add a subclass—so a switch over the hierarchy cannot be checked. The sealed modifier closes the hierarchy to the defining library, allowing the compiler to verify that every subtype is handled.
The evolution problem. A library that publishes a class wants to add methods to it without breaking downstream code. If the class is open for implementation, a downstream class may already define a member with the name the library wants to add. The final modifier prevents this by disallowing external subtypes entirely, giving the library author full control over the class’s members.
The default-openness problem. Before Dart 3, every class was open for extension and implementation. There was no way to express “this class is not designed for extension.” The modifiers make the design intent explicit. A class that is not marked is still open, but a class that is marked communicates its constraints to the reader and enforces them in the compiler.
a. base: extension without implementation
The base modifier allows a class to be extended but not implemented outside its own library. A class that extends or implements a base class must itself be marked base, final, or sealed.
base class Vehicle {
void start() {
print('Vehicle started');
}
}
base class Car extends Vehicle {
@override
void start() {
super.start();
print('Car ready');
}
}
The Car class extends Vehicle and repeats the base modifier. The super.start() call invokes the inherited implementation. The private members of Vehicle—if any—are available to Car because Car inherits them.
An external library cannot implement Vehicle:
// In another library
class Truck implements Vehicle { // Error
@override
void start() {
print('Truck started');
}
}
The error occurs because Vehicle is base and Truck is in a different library. The compiler rejects the implementation because Truck would not inherit Vehicle‘s implementation, which the base modifier guarantees.
The base modifier is used when the class has behavior that depends on its implementation being inherited. If a class uses private methods internally, or if it has fields that its methods depend on, base ensures that every subtype has those members.
A base class can be abstract, which allows it to declare abstract members that subtypes must implement while still requiring implementation inheritance for the concrete members:
abstract base class Shape {
double get area;
void describe() {
print('Area: $area');
}
}
The describe method uses the area getter, which is abstract. A subtype must implement area and inherits describe. The base modifier ensures that the subtype inherits the implementation of describe rather than reimplementing it.
b. interface: implementation without extension
The interface modifier allows a class to be implemented but not extended outside its own library. A class that implements an interface class must provide all the public members but inherits no implementation.
interface class Repository {
void save(String data) {
print('Saving: $data');
}
String load() {
return 'default';
}
}
An external class can implement Repository:
class MemoryRepository implements Repository {
@override
void save(String data) {
print('Memory saving: $data');
}
@override
String load() {
return 'memory';
}
}
The MemoryRepository provides its own implementation of both methods. It does not inherit the default implementations from Repository. The interface modifier allows this but prevents MemoryRepository from extending Repository:
class DiskRepository extends Repository { // Error
// ...
}
The error occurs because Repository is interface and DiskRepository is in a different library. The compiler rejects the extension because DiskRepository would inherit the implementation, which the interface modifier disallows.
The interface modifier is used when the class is a pure contract. It has no implementation that subtypes should inherit, and it is designed to be implemented, not extended. An interface class can be abstract, which makes it a pure interface with no implementation at all:
abstract interface class Drawable {
void draw();
void erase();
}
The Drawable class declares two methods with no implementation. Any class can implement it, but no class can extend it outside the library. This is the Dart 3 equivalent of a traditional interface.
c. final and sealed: closed hierarchies
The final modifier prevents a class from being extended or implemented outside its own library. It is the strongest restriction short of sealed.
final class Config {
final String host;
final int port;
Config(this.host, this.port);
void printConfig() {
print('$host:$port');
}
}
An external library cannot extend or implement Config:
class MyConfig extends Config { // Error
// ...
}
class FakeConfig implements Config { // Error
// ...
}
Both are rejected. The Config class is completely closed. Its members cannot be overridden, and its interface cannot be adopted. The library author can add new methods to Config without worrying about external subclasses defining members with the same names.
Within the same library, a subclass can extend a final class, but the subclass must also be base, final, or sealed:
final class SpecialConfig extends Config {
SpecialConfig(super.host, super.port);
}
The SpecialConfig class is in the same library and repeats the final modifier. The restriction applies across library boundaries, not within a library.
The sealed modifier restricts all subtypes to the same library and enables exhaustive pattern matching. A sealed class is implicitly abstract and cannot be instantiated directly.
sealed class Result {}
class Success extends Result {
final String data;
Success(this.data);
}
class Failure extends Result {
final String error;
Failure(this.error);
}
class Loading extends Result {}
All three subtypes are in the same library. The compiler knows that Result has exactly three subtypes, so a switch over a Result can be checked for exhaustiveness:
String describe(Result result) {
return switch (result) {
Success(data: var d) => 'Success: $d',
Failure(error: var e) => 'Failure: $e',
Loading() => 'Loading',
};
}
If a new subtype is added to the library, the switch becomes a compile error until the new case is handled. This is the same exhaustiveness guarantee that enums provide, extended to classes with data.
A sealed class can have abstract members that subtypes implement, and it can have concrete members that subtypes inherit. The subtypes can be final, base, or sealed, but not interface, because sealed already restricts subtyping to the library.
The sealed modifier is the foundation of Dart’s algebraic data type support. A sealed class with final subtypes models a closed set of alternatives, each with its own data, and the compiler ensures that every alternative is handled.
Complete Example Session
// ============================================
// PART 1: BASE CLASS
// ============================================
base class Vehicle {
void start() => print('Vehicle started');
}
base class Car extends Vehicle {
@override
void start() {
super.start();
print('Car ready');
}
}
// ============================================
// PART 2: BASE PREVENTS IMPLEMENTATION
// ============================================
// In another library:
// class Truck implements Vehicle {} // Error
// ============================================
// PART 3: INTERFACE CLASS
// ============================================
interface class Repository {
void save(String data) => print('Saving: $data');
String load() => 'default';
}
class MemoryRepository implements Repository {
@override
void save(String data) => print('Memory: $data');
@override
String load() => 'memory';
}
// ============================================
// PART 4: INTERFACE PREVENTS EXTENSION
// ============================================
// In another library:
// class DiskRepo extends Repository {} // Error
// ============================================
// PART 5: ABSTRACT INTERFACE
// ============================================
abstract interface class Drawable {
void draw();
void erase();
}
// ============================================
// PART 6: FINAL CLASS
// ============================================
final class Config {
final String host;
final int port;
Config(this.host, this.port);
}
// ============================================
// PART 7: FINAL PREVENTS BOTH
// ============================================
// In another library:
// class MyConfig extends Config {} // Error
// class FakeConfig implements Config {} // Error
// ============================================
// PART 8: SEALED CLASS
// ============================================
sealed class Result {}
final class Success extends Result {
final String data;
Success(this.data);
}
final class Failure extends Result {
final String error;
Failure(this.error);
}
final class Loading extends Result {}
// ============================================
// PART 9: EXHAUSTIVE SWITCH
// ============================================
String describe(Result result) {
return switch (result) {
Success(data: var d) => 'Success: $d',
Failure(error: var e) => 'Failure: $e',
Loading() => 'Loading',
};
}
// ============================================
// PART 10: ABSTRACT BASE
// ============================================
abstract base class Shape {
double get area;
void describe() => print('Area: $area');
}
base class Circle extends Shape {
final double radius;
Circle(this.radius);
@override
double get area => 3.14159 * radius * radius;
}
The ten parts covered base, base preventing implementation, interface, interface preventing extension, abstract interface, final, final preventing both, sealed, exhaustive switch, and abstract base.
Quick Reference
Modifier Effects
| Modifier | Extend Outside | Implement Outside | Exhaustive |
|---|---|---|---|
| (none) | Yes | Yes | No |
base | Yes | No | No |
interface | No | Yes | No |
final | No | No | No |
sealed | No | No | Yes |
Modifier Combinations
| Combination | Valid | Purpose |
|---|---|---|
abstract base | Yes | Base with abstract members |
abstract interface | Yes | Pure interface |
abstract final | Yes | Cannot be constructed or subtyped |
base interface | No | Mutually exclusive |
final interface | No | Mutually exclusive |
sealed base | No | Mutually exclusive |
Subtype Modifier Rules
| Superclass Modifier | Subclass Must Be |
|---|---|
base | base, final, or sealed |
interface | Any |
final | Same library only |
sealed | Same library only, final, base, or sealed |
When to Use Each
| Situation | Modifier |
|---|---|
| Share implementation, prevent implementation | base |
| Define a contract, prevent extension | interface |
| Close the hierarchy completely | final |
| Enable exhaustive pattern matching | sealed |
| Default behavior | No modifier |
Best Practices
✅ Do This:
// Use base when subtypes must inherit implementation
base class Vehicle { } // ✅
// Use interface for pure contracts
abstract interface class Repository { } // ✅
// Use final to close a class
final class Config { } // ✅
// Use sealed for exhaustive matching
sealed class Result { } // ✅
// Repeat the modifier on subtypes
base class Car extends Vehicle { } // ✅
// Use abstract with interface for pure contracts
abstract interface class Drawable { void draw(); } // ✅
// Use sealed with final subtypes
sealed class State { }
final class Idle extends State { } // ✅
❌ Don’t Do This:
// Don't combine mutually exclusive modifiers
base interface class Mixed { } // ❌
// Don't implement a base class externally
class External implements BaseClass { } // ❌
// Don't extend an interface class externally
class External extends InterfaceClass { } // ❌
// Don't forget the modifier on subtypes
base class Base { }
class Sub extends Base { } // Error // ❌
// Don't expect sealed to allow external subtypes
sealed class Closed { }
class External extends Closed { } // Error // ❌
// Don't use final when you need extension
final class Locked { }
class Sub extends Locked { } // Error // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Cannot implement base class | base prevents implementation | Use interface or no modifier |
| Cannot extend interface class | interface prevents extension | Use base or no modifier |
| Subclass missing modifier | Modifier not repeated | Add base, final, or sealed |
| Sealed subclass outside library | Sealed restricts to library | Move subtype to same library |
| Switch not exhaustive | Missing subtype case | Add the missing case |
| Mutually exclusive modifiers | Invalid combination | Use one of base, interface, final |
| Final class cannot be extended | final prevents subtyping | Remove final or use composition |
Real-World Examples
1. Base for Shared Implementation
base class Service {
void log(String msg) => print('[LOG] $msg');
}
base class UserService extends Service {
void create() => log('Creating user');
}
2. Interface for Contracts
abstract interface class PaymentProcessor {
Future<bool> charge(double amount);
}
3. Final for Value Objects
final class Money {
final double amount;
final String currency;
Money(this.amount, this.currency);
}
4. Sealed for State
sealed class AuthState {}
final class Authenticated extends AuthState {}
final class Unauthenticated extends AuthState {}
5. Sealed with Data
sealed class ApiResponse {}
final class Success extends ApiResponse {
final dynamic data;
Success(this.data);
}
final class Error extends ApiResponse {
final String message;
Error(this.message);
}
6. Abstract Base
abstract base class Shape {
double get area;
void describe() => print(area);
}
7. Abstract Interface
abstract interface class Serializable {
Map<String, dynamic> toJson();
}
8. Base with Private Members
base class Counter {
int _count = 0;
void increment() => _count++;
int get count => _count;
}
9. Sealed Switch
String render(AuthState state) => switch (state) {
Authenticated() => 'Logged in',
Unauthenticated() => 'Logged out',
};
10. Final Preventing Override
final class ApiKey {
final String value;
ApiKey(this.value);
}
Visual
Extend vs Implement
┌─────────────────────────────────────────────────────────────┐
│ EXTEND │
│ │
│ class Child extends Parent { } │
│ │
│ Inherits interface AND implementation. │
│ Can call super. │
│ Inherits private members. │
│ │
├─────────────────────────────────────────────────────────────┤
│ │
│ IMPLEMENT │
│ │
│ class Child implements Parent { } │
│ │
│ Inherits interface ONLY. │
│ Cannot call super. │
│ Does not inherit private members. │
│ │
└─────────────────────────────────────────────────────────────┘
Modifier Matrix
┌─────────────────────────────────────────────────────────────┐
│ MODIFIER EXTEND IMPLEMENT EXHAUSTIVE │
│ │
│ (none) ✓ ✓ ✗ │
│ base ✓ ✗ ✗ │
│ interface ✗ ✓ ✗ │
│ final ✗ ✗ ✗ │
│ sealed ✗ ✗ ✓ │
│ │
│ ✓ = allowed outside library │
│ ✗ = not allowed outside library │
│ │
└─────────────────────────────────────────────────────────────┘
Sealed Exhaustiveness
┌─────────────────────────────────────────────────────────────┐
│ sealed class Result {} │
│ │
│ final class Success extends Result {} │
│ final class Failure extends Result {} │
│ final class Loading extends Result {} │
│ │
│ All subtypes in the same library. │
│ Compiler knows the complete set. │
│ │
│ switch (result) { │
│ Success() => ..., │
│ Failure() => ..., │
│ Loading() => ..., │
│ } │
│ ✓ Exhaustive │
│ │
│ switch (result) { │
│ Success() => ..., │
│ Failure() => ..., │
│ } │
│ ✗ Error: Loading not handled │
│ │
└─────────────────────────────────────────────────────────────┘
Modifier Decision Tree
┌─────────────────────────────────────────────────────────────┐
│ Do you need exhaustive pattern matching? │
│ │ │
│ ├── YES ──▶ sealed │
│ │ │
│ └── NO │
│ │ │
│ ▼ │
│ Should subclasses inherit implementation? │
│ │ │
│ ├── YES ──▶ Should external code implement it? │
│ │ ├── YES ──▶ (no modifier) or base │
│ │ └── NO ──▶ base │
│ │ │
│ └── NO ──▶ Should external code implement it? │
│ ├── YES ──▶ interface │
│ └── NO ──▶ final │
│ │
└─────────────────────────────────────────────────────────────┘
Summary
| Modifier | Extend | Implement | Exhaustive |
|---|---|---|---|
| (none) | Yes | Yes | No |
base | Yes | No | No |
interface | No | Yes | No |
final | No | No | No |
sealed | No | No | Yes |
abstract base | Yes | No | No |
abstract interface | No | Yes | No |
Key takeaways:
- Class modifiers control subtyping across library boundaries. They are declared before the
classkeyword and enforced by the compiler. The restrictions apply to code in other libraries; code in the same library can use the class more freely. baseallows extension but not implementation. It is used when the class’s behavior depends on its implementation being inherited. A subtype must repeat thebasemodifier or usefinalorsealed.interfaceallows implementation but not extension. It is used for pure contracts. Anabstract interface classhas no implementation at all and is the Dart 3 equivalent of a traditional interface.finalallows neither extension nor implementation. It closes the class completely. The library author can add members without worrying about external subtypes. Subclasses within the same library must also bebase,final, orsealed.sealedenables exhaustive pattern matching. It restricts all subtypes to the same library, so the compiler knows the complete set. Aswitchover a sealed type must handle every subtype, or the code does not compile.- The modifiers are mutually exclusive in part.
base,interface, andfinalcannot be combined because they all control the same two capabilities.sealedcannot be combined with any of them. abstractcombines with the modifiers.abstract base,abstract interface, andabstract finalare all valid. Theabstractkeyword prevents direct construction but does not change the subtyping restrictions.- Subtypes must repeat the modifier. A class that extends a
baseclass must itself bebase,final, orsealed. A class that extends afinalorsealedclass must be in the same library. The compiler enforces these rules.
Remember: Class modifiers are a design tool, not a decoration. They let a library author express how a class is intended to be used and enforce that intent in the compiler. Use base when the class has implementation that subtypes must inherit. Use interface when the class is a pure contract. Use final when the class should not be subtyped at all. Use sealed when the class represents a closed set of alternatives and the compiler should verify that every alternative is handled. The modifiers are optional—a class without one is open—but marking a class communicates intent and prevents misuse. In a library that is used by other developers, the modifiers are the difference between an API that is documented and one that is enforced.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!