dart-seal-type-hierarchies
Skill148 starsChanged 14 days agoApache-2.0View on GitHub
Identify closed type hierarchies that are not declared `sealed`, and seal them so the compiler can enforce switch exhaustiveness. Covers the same-library requirement, the public-API breaking-change tradeoff, and the migration from `is` cascades to exhaustive switches.
In this repository11 other files it gives its agents.
- dart-best-practicesSkill
- dart-doc-validationSkill
- dart-long-linesSkill
- dart-matcher-best-practicesSkill
- dart-modern-featuresSkill
- dart-multiline-stringsSkill
- dart-package-maintenanceSkill
- dart-test-coverageSkill
- dart-test-fundamentalsSkill
- dash-discoverSkill
- profile-dart-codeSkill
The file
We split this skill into the rules it gives an agent
9 rules. About 0 are common, 5 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 18
Use this skill when:
- Distinctive: line 30
Do NOT seal a type when:
- Distinctive: line 41
If callers construct the base type directly, sealing it breaks them, and the fix is a larger refactor than this skill covers.
- Very specific: line 46
Check environment.sdk in pubspec.yaml first.
- Distinctive: line 74
Every direct subtype is declared in the same library as the base type.
- Very specific: line 143
Add sealed to the base type declaration.
- Very specific: line 144
Run dart analyze. Every non-exhaustive switch over the hierarchy now reports an error.
- Very specific: line 148
Consider marking leaf subtypes final to prevent further extension.
- Distinctive: line 156
Do not treat this skill as a reason to seal a hierarchy you do not own the evolution of.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Seal Type Hierarchies
1. When to use this skill
Use this skill when:
- A package declares an abstract base type whose subtypes are all defined in the
same library, but the base type is not marked
sealed. - Code branches over the members of such a hierarchy with
ischecks or a non-exhaustiveswitch, so adding a subtype later fails silently at runtime instead of loudly at compile time. - Reviewing a new type hierarchy and deciding whether it should be open to external subtyping.
When NOT to use (Abstention Guardrails)
Do NOT seal a type when:
- Subtypes span multiple libraries:
sealedrequires every direct subtype to be declared in the same library as the base type. If subtypes live in other files (and are notpart ofthe same library), the code will not compile. Either move them or leave the hierarchy open. - The type is public API and external extension is intended: Sealing a type exported from a published package is a breaking change for any downstream package that extends or implements it. Plugin interfaces, visitor bases, and extension points are meant to be open. Do not seal them to win exhaustiveness.
- The base type is concrete and instantiated:
sealedimpliesabstract. If callers construct the base type directly, sealing it breaks them, and the fix is a larger refactor than this skill covers. - Fewer than two subtypes: A single subtype is specialization, not an algebraic hierarchy. Exhaustiveness checking buys nothing.
- SDK constraint below 3.0.0: Class modifiers do not exist before Dart 3.
Check
environment.sdkinpubspec.yamlfirst.
2. Why sealing is the correctness move
The value is not switch syntax. It is that the compiler starts rejecting incomplete branching:
seal the hierarchy -> exhaustiveness checking -> adding a subtype becomes
a compile error at every switch, instead of a silent fallthroughAn unsealed hierarchy with is cascades is not wrong today. It becomes wrong
the moment someone adds a subtype, and nothing tells them which call sites they
missed. Sealing is prophylactic: it converts a future runtime bug into a
present-day compile error.
This is a different concern from preferring pattern matching for readability. Pattern matching over an unsealed hierarchy is a style choice. Sealing the hierarchy is a correctness guarantee, and the ergonomic payoff is a consequence, not the goal.
3. Detection
A type is a candidate when all of the following hold:
- It is declared
abstractand is not alreadysealed. - It has two or more direct subtypes.
- Every direct subtype is declared in the same library as the base type.
- Sealing it is not a breaking change: it lives under
lib/src/, or is already markedfinal, or the package is an application rather than a published library.
Conditions 1 through 3 are decidable from the syntax tree. Condition 4 requires knowing the author's intent about the public API and is the part a human or an LLM must confirm.
This detection is not expressible as a single-file regular expression. It is
a question about the package-wide type graph: you must collect every type
declaration and every extends/implements/with edge before you can tell
whether a hierarchy is closed.
4. Applying the change
Before
abstract class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Square extends Shape {
final double side;
Square(this.side);
}
double area(Shape shape) {
if (shape is Circle) {
return 3.14159 * shape.radius * shape.radius;
} else if (shape is Square) {
return shape.side * shape.side;
}
// Silently reached when a new subtype is added.
throw ArgumentError('Unknown shape: $shape');
}After
sealed class Shape {}
final class Circle extends Shape {
final double radius;
Circle(this.radius);
}
final class Square extends Shape {
final double side;
Square(this.side);
}
double area(Shape shape) => switch (shape) {
Circle(:final radius) => 3.14159 * radius * radius,
Square(:final side) => side * side,
};Adding class Triangle extends Shape now produces a compile error at area,
naming the missing case. The throw and its unreachable-by-construction error
message are gone.
Migration steps
- Add
sealedto the base type declaration. - Run
dart analyze. Every non-exhaustive switch over the hierarchy now reports an error; every subtype declared outside the library reports one too. - Convert
iscascades toswitchexpressions, removing the trailingthrow/defaultthat existed only to satisfy the return type. - Consider marking leaf subtypes
finalto prevent further extension.
5. Interaction with other skills
dart-use-pattern-matching/dart-modern-features: apply after sealing. Rewriting aniscascade as a switch over an unsealed type is a readability change only; the same rewrite over a sealed type is checked by the compiler.- Do not treat this skill as a reason to seal a hierarchy you do not own the evolution of.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

