Dart Idioms skill for Codex

Sealed types, patterns and null safety without bangs. Codex reads the SDK constraint in pubspec.yaml and uses only the Dart features that language version allows. Domain states become sealed classes matched by exhaustive switch expressions, JSON is destructured with patterns, nullable values are handled at the boundary instead of with the bang operator, and every change passes dart analyze with strict casts and unawaited-future checks.

  • Languages
  • $dart-idioms
  • 184 lines
  • 2 files
  • ~1,750 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- dart-idioms

Installs to ~/.agents/skills/dart-idioms/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $dart-idioms, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes idiomatic Dart 3 - sound null safety without bang-operator sprinkling, records and destructuring, patterns and exhaustive switch expressions over sealed classes, class modifiers, extension types, async/await with Futures and Streams, isolates for CPU work - kept clean with dart analyze, dart fix, dart format and analysis_options.yaml lints. Use when writing or reviewing Dart packages, CLIs or the non-widget logic of a Flutter app, migrating pre-Dart-3 code, or fixing analyzer warnings and null-safety errors.

Use it when

  • Writing Dart packages, CLIs or server code
  • Cleaning up the business logic layer of a Flutter app
  • Migrating older Dart code to Dart 3 records, patterns and class modifiers
  • Null-safety errors or analyzer warnings need fixing

Use something else when

How to check the result

dart analyze --fatal-infos and the format check pass, and tests cover each variant of any sealed type that changed.

The mistake it prevents

Adding a default arm to a switch over a sealed class, which silences the compiler error that would flag unhandled new variants.

~/.agents/skills/dart-idioms/SKILL.md

--- name: dart-idioms description: Writes idiomatic Dart 3 - sound null safety without bang-operator sprinkling, records and destructuring, patterns and exhaustive switch expressions… license: MIT ---

Idiomatic Dart 3 code makes illegal states unrepresentable (sealed hierarchies, exhaustive switches, non-nullable by default), uses ! only where a comment could explain why the value cannot be null, and passes dart analyze with the project’s lint set and zero warnings.

Before changing code

  1. Read AGENTS.md, pubspec.yaml (the environment: sdk: lower bound sets the language version and therefore which features compile), pubspec.lock and analysis_options.yaml.
  2. dart --version; for Flutter projects use flutter analyze / flutter test, which wrap the same tools.
  3. dart pub get needs network access unless the pub cache is warm; ask first in a restricted sandbox.

Feature map

FeatureLanguage versionUse for
Records (int, String), ({int x, int y})3.0returning several values without a class
Patterns, switch expressions, if-case3.0destructuring JSON, exhaustive branching
sealed, final, base, interface, mixin class modifiers3.0closed hierarchies and API control
Private final field promotion3.2if (_x != null) use(_x) works on private finals
Extension types3.3zero-cost wrappers (IDs, units) over existing types
Digit separators 1_000_0003.6readable literals
Wildcard _ variables (non-binding)3.7ignored params (_, _) => 0
Null-aware elements [?maybe]3.8building lists and maps from nullable values
Dot shorthands .active where the type is known3.10enum and static member shorthand

Do not use a feature newer than the SDK lower bound in pubspec.yaml; raise the bound deliberately (it also changes formatter style, see below).

Modelling with sealed classes and patterns

sealed class PaymentResult {}

final class Approved extends PaymentResult {
  Approved(this.transactionId);
  final String transactionId;
}

final class Declined extends PaymentResult {
  Declined(this.reason, {this.retryable = false});
  final String reason;
  final bool retryable;
}

final class NeedsAction extends PaymentResult {
  NeedsAction(this.url);
  final Uri url;
}

String describe(PaymentResult r) => switch (r) {
      Approved(:final transactionId) => 'Approved ($transactionId)',
      Declined(retryable: true) => 'Declined, try again',
      Declined(:final reason) => 'Declined: $reason',
      NeedsAction(:final url) => 'Continue at $url',
    };

Adding a subclass makes every non-exhaustive switch a compile error, which is the point. Avoid a default/_ arm on sealed types; it silences that check.

Parsing JSON with patterns

User parseUser(Object? json) {
  if (json case {'id': int id, 'name': String name, 'email': String? email}) {
    return User(id: id, name: name, email: email);
  }
  throw FormatException('Invalid user payload: $json');
}

For large schemas, use code generation (json_serializable, freezed) that the project already has instead of hand-written patterns.

Null safety without !

Instead ofPrefer
user!.name after a separate null check on a fieldcopy to a local: final u = user; if (u != null) u.name (public fields do not promote)
late for values that may never be setnullable type and handle null, or set it in the constructor
map[key]!map[key] ?? fallback, or if (map[key] case final v?)
list.firstWhere(..., orElse: () => null) (does not compile)list.firstWhereOrNull from package:collection, or where(...).firstOrNull
String? name passed through five layersvalidate at the boundary and use non-nullable types inside

late final is fine for values initialised exactly once before use (e.g. in initState); reading too early throws LateInitializationError at runtime.

Records and extension types

(double lat, double lng) parseCoord(String s) {
  final [a, b] = s.split(',');
  return (double.parse(a), double.parse(b));
}

final (lat, lng) = parseCoord('51.5,-0.12');

extension type const UserId(String value) {
  UserId.parse(String raw) : value = raw.trim() {
    if (!value.startsWith('usr_')) throw FormatException('bad id: $raw');
  }
}

The list pattern final [a, b] = ... throws if the split does not produce exactly two parts; that is a feature at a parsing boundary. Extension types vanish at runtime (is checks see the underlying type), so they document intent but do not secure it.

Async

  • Every Future is awaited, returned, or explicitly unawaited(...) (from dart:async); enable the unawaited_futures lint.
  • Parallel independent work: final (a, b) = await (fetchA(), fetchB()).wait; (record .wait, 3.0+) or Future.wait.
  • Timeouts: await call().timeout(const Duration(seconds: 5)), and handle TimeoutException.
  • Streams: cancel subscriptions you start (StreamSubscription.cancel() in dispose); prefer await for in non-UI code.
  • CPU-bound work blocks the event loop; use Isolate.run(() => heavyParse(bytes)) (Flutter: compute). Isolates do not share memory; pass and return plain data.

Lints and formatting

# analysis_options.yaml
include: package:lints/recommended.yaml   # Flutter: package:flutter_lints/flutter.yaml

analyzer:
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true
  errors:
    unawaited_futures: error

linter:
  rules:
    - unawaited_futures
    - prefer_final_locals
    - avoid_dynamic_calls
dart analyze --fatal-infos     # zero issues is the bar
dart fix --dry-run             # preview automated fixes, then: dart fix --apply
dart format .                  # style follows the package's language version
dart test                      # or flutter test

Since Dart 3.7 the formatter uses the “tall” style for code at language version 3.7+. Raising the SDK lower bound can reformat the whole package; do that in its own commit.

Gotchas

  • == on classes is identity unless overridden; records compare structurally. For data classes, override ==/hashCode together or use codegen.
  • const constructors make canonical instances; const [] lists are unmodifiable and throw on add.
  • dynamic turns off checking; prefer Object? and pattern-match it.
  • int is 64-bit on the VM but follows JS number semantics on the web: large IDs lose precision when compiled to JS; keep them as String.
  • Top-level and static variables are lazily initialised; side effects in their initialisers run on first access, not at startup.
  • catch (e) catches everything including Errors (programming bugs); catch specific exceptions with on FormatException catch (e).

Verification

  • dart analyze --fatal-infos (or flutter analyze) is clean.
  • dart format --output=none --set-exit-if-changed . passes.
  • Tests pass; sealed-type changes have a test per variant.
  • Report the SDK constraint and any language features introduced.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Dart Idioms"
  short_description: "Sealed types, patterns and null safety without bangs"
  default_prompt: "$dart-idioms Refactor these Dart result classes into a sealed class with switch expressions"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
New

Introducing Codex Skills Pro

79 professional skills that take Codex through complete engagements, each with its own scripts, references and a report that verifies the result.

All-AccessOne-time payment, no subscription
$49
Get All-AccessOr browse individual skills from $2.99 →

Secure checkout by Creem