Dart (3.13) Module 9 – Inheritance, mixins, class modifiers and extensions
Class modifiers: base, interface, final, sealed
Control how other libraries use your Dart classes with base, interface, final and sealed modifiers, added in Dart 3.0 to protect API boundaries.
What you will learn
- Predict which class modifier prevents a given subtype relationship
- Explain why a subtype of a base class must itself be base, final, or sealed
- Choose between final and sealed when closing a type hierarchy
- Plan how adding a modifier to a published class affects its users
Before you start
What you’ll learn · Before you start
What you will learn
- Predict which class modifier prevents a given subtype relationship
- Explain why a subtype of a base class must itself be base, final, or sealed
- Choose between final and sealed when closing a type hierarchy
- Plan how adding a modifier to a published class affects its users
Before you start
On this page
A library exports a class that six other packages implement. When the library adds a new method, every implementer breaks: the method is missing. Before Dart 3.0, the library author could only hope users noticed the documentation. Since Dart 3.0, class modifiers let you say what other libraries may do with your class, and the compiler enforces the rule.
Four modifiers control subtyping across library boundaries: base class: A class marked base that can be extended but not implemented outside its library; subtypes must also be base, final, or sealed. allows extending but
not implementing, interface class: A class marked interface that can be implemented but not extended outside its library, useful when you want users to provide their own implementations. allows implementing but not extending,
final class: A class marked final that cannot be extended or implemented outside its library, closing the type hierarchy entirely. closes the hierarchy entirely, and sealed closes it while enabling exhaustive
switches. This lesson shows how each modifier protects an API boundary, which combinations compile, and how to migrate
a published class without breaking its users. Every error message on this page comes from Dart 3.13.5.
Modifiers limit what other libraries may do
A class without a modifier is open: code outside its library can construct it, extend it, implement it and, if it is also a mixin class, mix it in. Each modifier removes one or more of those abilities for code outside the library. Inside the same library extending and implementing stay open, with two rules the next section shows.
Here is a small payments library with one class of each kind, and a client file that uses them:
/// A small payments library showing class modifiers.
library payments;
/// A final class: cannot be extended or implemented outside its library.
final class Money {
final int paise;
const Money(this.paise);
Money operator +(Money other) => Money(paise + other.paise);
@override
String toString() => 'Money($paise paise)';
}
/// An interface class: can be implemented but not extended outside its library.
interface class Gateway {
bool process(Money amount) {
print('Gateway processed $amount');
return true;
}
}
/// A base class: can be extended but not implemented outside its library.
/// Subtypes must be base, final, or sealed.
base class Ledger {
final List<Money> _entries = [];
void record(Money amount) {
_entries.add(amount);
print('Ledger recorded $amount');
}
Money get total => _entries.fold(Money(0), (a, b) => a + b);
}
/// A sealed class: cannot be extended or implemented outside its library.
/// The compiler knows all direct subtypes (for exhaustive switches).
sealed class PaymentResult {}
class Success extends PaymentResult {
final Money amount;
Success(this.amount);
}
class Failure extends PaymentResult {
final String reason;
Failure(this.reason);
} /// Client code using the payments library with class modifiers.
import 'payments.dart';
/// This compiles: Gateway can be implemented (interface class).
class TestGateway implements Gateway {
@override
bool process(Money amount) {
print('TestGateway processed $amount');
return true;
}
}
/// This compiles: Ledger can be extended (base class).
/// The subtype must be marked base, final, or sealed.
base class AppLedger extends Ledger {
@override
void record(Money amount) {
print('[app] Recording $amount');
super.record(amount);
}
}
void main() {
// Money can be constructed (final allows construction).
final payment = Money(10000);
print('Payment: $payment');
// Gateway can be constructed and implemented.
final gateway = TestGateway();
gateway.process(payment);
// Ledger can be extended.
final ledger = AppLedger();
ledger.record(Money(1000));
ledger.record(Money(2500));
print('Total: ${ledger.total}');
} Output
Payment: Money(10000 paise) TestGateway processed Money(10000 paise) [app] Recording Money(1000 paise) Ledger recorded Money(1000 paise) [app] Recording Money(2500 paise) Ledger recorded Money(2500 paise) Total: Money(3500 paise)
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run app.dart
The client constructed a Money, implemented a Gateway, and extended a Ledger, all of which the modifiers allow.
The table below summarises what each modifier permits from another library:
| Modifier | Construct | Extend | Implement | Mix in |
|---|---|---|---|---|
| (none) | yes | yes | yes | no |
base |
yes | yes | no | no |
interface |
yes | no | yes | no |
final |
yes | no | no | no |
sealed |
no | no | no | no |
abstract |
no | yes | yes | no |
mixin |
no | no | yes | yes |
A class modifier: A keyword before a class declaration that controls what other libraries may do with it: base, interface, final, sealed, abstract or mixin. controls the relationship between a class and code in other
libraries. The modifier’s name hints at its effect: base forces other code to build
on your code as a base, keeping your implementation; interface lets other code match your interface without
inheriting your implementation; final ends the hierarchy; sealed closes it for exhaustive switching.
What happens when client code tries something a modifier forbids? The compiler refuses to build it:
/// A final class cannot be extended outside its library.
import 'payments.dart';
class ExtendMoney extends Money {
ExtendMoney(super.paise);
}
void main() {} Output (exit status 254)
Printed as an error (standard error)
final_extend_fails.dart:4:27: Error: The class 'Money' can't be extended outside of its library because it's a final class.
class ExtendMoney extends Money {
^
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run final_extend_fails.dart
/// An interface class cannot be extended outside its library.
import 'payments.dart';
class ExtendGateway extends Gateway {
@override
bool process(Money amount) => true;
}
void main() {} Output (exit status 254)
Printed as an error (standard error)
interface_extend_fails.dart:4:29: Error: The class 'Gateway' can't be extended outside of its library because it's an interface class.
class ExtendGateway extends Gateway {
^
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run interface_extend_fails.dart
/// A base class cannot be implemented outside its library.
import 'payments.dart';
base class FakeLedger implements Ledger {
@override
void record(Money amount) {}
@override
Money get total => Money(0);
}
void main() {} Output (exit status 254)
Printed as an error (standard error)
base_implement_fails.dart:4:34: Error: The class 'Ledger' can't be implemented outside of its library because it's a base class.
base class FakeLedger implements Ledger {
^
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run base_implement_fails.dart
Each message names the modifier and what it forbids, and each says “outside of its library”: the next section shows what changes inside it.
Checkpoint
Check: What each modifier permits
Inside its own library a class can be extended and implemented
The limits on extending and implementing apply only to other libraries: code in the same library may extend or implement a class whatever its modifier. This matters because a sealed class is usually declared next to its subtypes, and those subtypes must be able to extend or implement it.
In payments.dart above, Success and Failure both extend the sealed class PaymentResult, which is legal because
they are in the same library. Move Success to a separate library and it fails: the class can’t be extended outside
its library because it is sealed. The sealed class: A class marked sealed: it cannot be extended or implemented outside its own library, so the compiler knows all its direct subtypes and can check that a switch over them is exhaustive. More in “Validating JSON in Dart with patterns” modifier is covered in detail in
Sealed classes and exhaustive switches.
Two rules hold inside the library too: a subtype of a base or final class must itself be base, final or
sealed, and only a mixin or a mixin class may follow with. This file declares three modified classes and five
subtypes:
/// One library that builds on its own modified classes.
/// The first three subtypes compile; the last two are refused even here.
final class Money {
final int paise;
const Money(this.paise);
}
interface class Gateway {
bool process(Money amount) => amount.paise > 0;
}
sealed class PaymentResult {}
// Allowed in the declaring library: extend an interface class, extend a sealed class,
// and extend a final class with a subtype that is itself final.
class LoggingGateway extends Gateway {}
class Success extends PaymentResult {}
final class Rupees extends Money {
const Rupees(int rupees) : super(rupees * 100);
}
// Refused in the declaring library too: a subtype of a final class without base, final or sealed,
// and a class that is not a mixin used after with.
class Token extends Money {
const Token(super.paise);
}
class Audited with Gateway {}
void main() {} Output (exit status 254)
Printed as an error (standard error)
same_library_fails.dart:26:7: Error: The type 'Token' must be 'base', 'final' or 'sealed' because the supertype 'Money' is 'final'.
Try adding 'base', 'final', or 'sealed' to the type.
class Token extends Money {
^
same_library_fails.dart:30:20: Error: The class 'Gateway' can't be used as a mixin because it isn't a mixin class nor a mixin.
class Audited with Gateway {}
^
Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run same_library_fails.dart
The compiler accepted three: LoggingGateway extends an interface class, Success a sealed class, and Rupees the
final Money while being final itself. It refused Token at line 26, which extends Money with no modifier, and
Audited at line 30, which puts a class that is not a mixin after with.
Key idea
A class modifier sets what other libraries may do. Inside your own library any class may be extended and
implemented, but a subtype of a base or final class must still be base, final or sealed.
Checkpoint
Check: What still applies inside the declaring library
Rules that spread to subtypes and combinations that are refused Pro: opens with a Pro, Premium or Ultimate pass
This part covers base transitivity, why base protects private members, the difference between interface and
abstract interface class, using final for value types, and which modifier combinations the language allows. Where
the compiler refuses something, the section shows its exact message from Dart 3.13.5 and the reason for the rule.
In this part:
- Base transitivity: subtypes inherit the restriction
- Interface versus abstract interface class
- Final class for value types
- Combinations the language refuses
2 runnable examples (Dart) · about 7 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Evolving a library API without breaking users Pro: opens with a Pro, Premium or Ultimate pass
Adding a modifier to a published class is a breaking change: client code that was allowed before becomes a compile error. This part shows how to deprecate a subtype relationship before removing it, the deprecation constructors added in Dart 3.10, and how to plan a migration with semantic versions.
In this part:
- Adding a modifier is a breaking change
- Deprecate first, remove later
- The SDK’s own examples: IOOverrides and RegExp
- Plan the migration
4 runnable examples (Dart) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Practise: predict which client snippets compile
Five snippets below try to subtype a class from another library. Predict which compile and which fail, then compare with the analysed result:
#!/usr/bin/env python3
"""Session driver to analyse five snippets that try to subtype modified classes.
Each line of the script is one command typed into one shell, so the snippet
files are created with write() before the session runs rather than with a shell
heredoc (which the line-by-line driver cannot keep together)."""
import session
session.files("modifiers")
session.write("snippet1.dart", """import 'modifiers/payments.dart';
// Money is final: extending it outside its library is an error.
class ExtendMoney extends Money {
ExtendMoney(super.paise);
}
void main() {}
""")
session.write("snippet2.dart", """import 'modifiers/payments.dart';
// Gateway is an interface class: implementing it outside its library is allowed.
class MockGateway implements Gateway {
@override
bool process(Money amount) => true;
}
void main() {}
""")
session.write("snippet3.dart", """import 'modifiers/payments.dart';
// Ledger is a base class: implementing it outside its library is an error,
// even when the implementer is itself base.
base class FakeLedger implements Ledger {
@override
void record(Money amount) {}
@override
Money get total => Money(0);
}
void main() {}
""")
session.write("snippet4.dart", """import 'modifiers/payments.dart';
// Ledger is a base class: extending it is allowed when the subtype is base.
base class AppLedger extends Ledger {
@override
void record(Money amount) {
super.record(amount);
}
}
void main() {}
""")
session.write("snippet5.dart", """import 'modifiers/payments.dart';
// PaymentResult is sealed: extending it outside its library is an error.
class CustomResult extends PaymentResult {}
void main() {}
""")
session.run("""
# Snippet 1: extend a final class -> error
! dart analyze snippet1.dart
# Snippet 2: implement an interface class -> compiles
dart analyze snippet2.dart
# Snippet 3: implement a base class from another library -> error
! dart analyze snippet3.dart
# Snippet 4: extend a base class with a base subtype -> compiles
dart analyze snippet4.dart
# Snippet 5: extend a sealed class -> error
! dart analyze snippet5.dart
""") Output
# Snippet 1: extend a final class -> error $ dart analyze snippet1.dart Analyzing snippet1.dart... error - snippet1.dart:4:27 - The class 'Money' can't be extended outside of its library because it's a final class. - invalid_use_of_type_outside_library 1 issue found. [exit status 3] # Snippet 2: implement an interface class -> compiles $ dart analyze snippet2.dart Analyzing snippet2.dart... No issues found! # Snippet 3: implement a base class from another library -> error $ dart analyze snippet3.dart Analyzing snippet3.dart... error - snippet3.dart:5:34 - The class 'Ledger' can't be implemented outside of its library because it's a base class. - invalid_use_of_type_outside_library 1 issue found. [exit status 3] # Snippet 4: extend a base class with a base subtype -> compiles $ dart analyze snippet4.dart Analyzing snippet4.dart... No issues found! # Snippet 5: extend a sealed class -> error $ dart analyze snippet5.dart Analyzing snippet5.dart... error - snippet5.dart:4:28 - The class 'PaymentResult' can't be extended, implemented, or mixed in outside of its library because it's a sealed class. - invalid_use_of_type_outside_library 1 issue found. [exit status 3]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 predict_session.py
Practice that adapts to you
Practise this lesson
4 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): when would you pick final class, and when sealed class, for a new Dart type?
Both prevent extending and implementing outside the library, but sealed also prevents constructing and makes the
class implicitly abstract. The key difference is intent: final closes the hierarchy when you want users to use
instances but never subtype them, while sealed closes it when you want the compiler to know every subtype for
exhaustive switches. A final class Money can be constructed; a sealed class PaymentResult cannot, and its
subtypes appear in the same library so a switch over them is exhaustive.
Two longer questions from this module’s bank cover adding modifiers to a published library and the rules that spread to subtypes:
-
Mid to senior
A payments library published version 1.0.0 with an open Gateway class. Some users extend it, others implement it. You want to add interface to Gateway in the next release. Plan the migration so that users have time to adapt, and explain which code breaks and when.
-
Mid to staff
Your library exports base class Ledger. A user's code compiles today with class AppLedger extends Ledger. In a new release you add a private method _auditEntry() that record() calls. Does the user's code still compile, and if not, what do they see?
-
Senior to staff
You maintain a Dart package with a class that users extend, implement, and mix in. You want to restrict it to implementing only. Describe the deprecation and release sequence, naming the exact annotation constructors available since Dart 3.10 and what each one warns about.
Flashcards · 6 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why does adding a method to an exported class break the packages that implement it?
Every implementer must provide every member, so the new method is missing in each of them. Since Dart 3.0, class modifiers let a library say what other libraries may do with its class, and the compiler enforces it.
-
What do base, interface, final and sealed allow another library to do?
base allows extending but not implementing, interface implementing but not extending, final neither, and sealed neither while it enables exhaustive switches.
-
What may another library do with a class that has no modifier?
Construct it, extend it and implement it. It may not mix it in unless the class is a mixin class.
-
Which classes can another library not construct?
sealed and abstract classes, and a mixin. Classes marked base, interface or final can still be constructed.
-
Inside its own library, may code extend a sealed or final class?
Yes: the limits apply only to other libraries, which is how a sealed class's subtypes sit next to it. Two rules still hold: a subtype of a base or final class must be base, final or sealed, and only a mixin or mixin class may follow with.
-
For a new type, when final class and when sealed class?
Both stop other libraries from extending and implementing it. A final class can be constructed and ends the hierarchy; a sealed class is implicitly abstract and its subtypes in the same library make switches over it exhaustive.
Key takeaways
- Class modifiers control what other libraries may do with a class:
baseallows extending,interfaceallows implementing,finalallows neither, andsealedcloses the hierarchy for exhaustive switching. - Inside its own library any class can be extended and implemented, but a subtype of a
baseorfinalclass must still bebase,finalorsealed, and only a mixin or mixin class can followwith. - A subtype of a
baseclass must itself bebase,final, orsealed, which is why base transitivity spreads down the hierarchy. - Adding a modifier to a published class is a breaking change: A change that makes code that compiled before stop compiling or behave differently, requiring a new major version under semantic versioning.: client code that compiled against the old version stops compiling against the new one.
Terms in this lesson
- base class
- A class marked base that can be extended but not implemented outside its library; subtypes must also be base, final, or sealed.
- breaking change
- A change that makes code that compiled before stop compiling or behave differently, requiring a new major version under semantic versioning.
- class modifier
- A keyword before a class declaration that controls what other libraries may do with it: base, interface, final, sealed, abstract or mixin.
- final class
- A class marked final that cannot be extended or implemented outside its library, closing the type hierarchy entirely.
- interface class
- A class marked interface that can be implemented but not extended outside its library, useful when you want users to provide their own implementations.
References
- Language: class modifiers (The Dart project (Google))
- Class modifiers for API maintainers (The Dart project (Google))
- Class modifiers reference (The Dart project (Google))
- Deprecated class (extend, implement, mixin, subclass constructors) (The Dart project (Google))
- Dart SDK CHANGELOG (The Dart project (Google))
- Dart language evolution (The Dart project (Google))
Report a problem with this lesson
Kept only in this browser. Your Learn progress