Class modifiers
Start by separating the three relationships — modifiers exist precisely to allow or forbid them:
extends— you inherit the implementation. Only one superclass,super.method()works, constructors chain.implements— you take only the interface contract. In Dart every class implicitly defines an interface of the same name, so any concrete class can be implemented; but then you must write every member yourself, nothing is inherited.with— you apply a mixin: implementation comes along, without building an "is-a" hierarchy, and you can apply as many as you like.
Until Dart 3 this freedom was total: a consumer of your package could extend or implement any of your classes, and adding a method to your package broke their code. Class modifiers arrived exactly to fix that.
| Modifier | Allowed OUTSIDE the library | What it is for |
|---|---|---|
| `class` (no modifier) | new, extends, implements | The Dart 2 behaviour — most permissive, least protected |
| `abstract class` | extends, implements (no new) | An incomplete type that cannot be instantiated directly |
| `base class` | extends only; subtypes must themselves be base/final/sealed | Guarantees every instance carries your implementation |
| `interface class` | implements only | Hand out a contract without handing out the implementation |
| `final class` | neither extends nor implements — use only | Freedom to add members later without a breaking change |
| `sealed class` | nothing (implicitly abstract + final) | The full set of subtypes is known — exhaustive switch |
| `mixin class` | extends, implements and `with` | A type usable both as a class and as a mixin |
Sealed classes — the headline Dart 3 feature. sealed makes a class abstract and final at once, and additionally tells the compiler: every direct subtype of this type lives in this same library.
Two consequences follow:
- A
switchover it is checked for exhaustiveness: cover all the variants and you need nodefaultbranch. - When you add a new variant, every
default-less switch stops compiling — the compiler hands you the list of places to fix. That is turning a runtime bug into a compile-time error.
In modern Dart, models like Result, LoadState and AuthState are written exactly as a sealed hierarchy plus pattern matching. Combinations are possible and meaningful too: abstract interface class is the pure-interface idiom; sealed combines with nothing, because it already carries abstract + final.
Which one should a library author pick? A practical rule:
- Inside application code you usually need no modifier at all — the team sees the whole codebase anyway.
- If you are writing a package, default to `final`. Removing
finallater is not a breaking change; adding it is. - If you only want to hand out a contract (consumers write their own implementation, tests build fakes) —
abstract interface class. - If every subtype must carry your state and invariants —
base. - If you are modelling a closed set of variants —
sealed.
Note that base is "contagious": every subtype of a base class must itself be base, final or sealed. That is deliberate — nobody may open a hole in the implementation chain with implements.
Interview tip. Two questions are near-guaranteed. First: "what is the difference between `extends` and `implements`?" — a strong answer must mention that in Dart every class implicitly defines an interface, so implements works on concrete classes too, you just inherit no implementation; that is the simplest way to build a fake in a test. Second: "when do you prefer a sealed class over an enum?" — compress it to one sentence: sealed when each variant carries its own data, enum when it is just a list of names. The most common mistake is explaining sealed as a synonym for "final class". The difference is exhaustiveness — final only closes the type, sealed also gives the compiler the complete list.
📚 Sources and documentation
- Class modifiersofficialdart.dev