dart-best-practices
General best practices for Dart development. Covers code style, effective Dart, and language features.
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.
$ npx -y skills add kevmoo/dash_skills --skill dart-seal-type-hierarchies --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/dart-seal-type-hierarchiesContext preview
The summary Claude sees to decide when to auto-load this skill.
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.
name: dart-seal-type-hierarchies description: |- 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. key_features: - Closed hierarchy detection - Exhaustiveness enforcement - Public API breaking-change analysis
Use this skill when:
same library, but the base type is not marked `sealed`.
non-exhaustive `switch`, so adding a subtype later fails silently at runtime instead of loudly at compile time.
external subtyping.
Do NOT seal a type when:
to be declared in the same library as the base type. If subtypes live in other files (and are not `part of` the same library), the code will not compile. Either move them or leave the hierarchy open.
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.
If callers construct the base type directly, sealing it breaks them, and the fix is a larger refactor than this skill covers.
algebraic hierarchy. Exhaustiveness checking buys nothing.
Check `environment.sdk` in `pubspec.yaml` first.
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 fallthrough
An 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.
A type is a candidate when all of the following hold:
1. It is declared `abstract` and is not already `sealed`. 2. It has two or more direct subtypes. 3. Every direct subtype is declared in the **same library** as the base type. 4. Sealing it is not a breaking change: it lives under `lib/src/`, or is already marked `final`, 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.
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');
}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.
1. Add `sealed` to the base type declaration. 2. Run `dart analyze`. Every non-exhaustive switch over the hierarchy now reports an error; every subtype declared outside the library reports one too. 3. Convert `is` cascades to `switch` expressions, removing the trailing `throw`/`default` that existed only to satisfy the return type. 4. Consider marking leaf subtypes `final` to prevent further extension.
sealing. Rewriting an `is` cascade as a switch over an unsealed type is a readability change only; the same rewrite over a sealed type is checked by the compiler.
evolution of.
General best practices for Dart development. Covers code style, effective Dart, and language features.
Best practices for validating Dart documentation comments. Covers using `dart doc` to catch unresolved references and macros.
Guidelines for handling long lines in Dart code to adhere to the 80-column rule. The `lines_longer_than_80_chars` lint.
Best practices for using `expect` and `package:matcher`. Focuses on readable assertions, proper matcher selection, and avoiding…
Guidelines for using modern Dart features (v3.0 - v3.10) such as Records, Extension Types, Class Modifiers, Wildcards, Digit…
Guidelines and best practices for refactoring consecutive prints, single-line string concatenations, and complex output blocks…