Sparround

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.

ModifierAllowed OUTSIDE the libraryWhat it is for
`class` (no modifier)new, extends, implementsThe 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/sealedGuarantees every instance carries your implementation
`interface class`implements onlyHand out a contract without handing out the implementation
`final class`neither extends nor implements — use onlyFreedom 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 switch over it is checked for exhaustiveness: cover all the variants and you need no default branch.
  • 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 final later 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