Pattern matching Dart 3 : booste ta logique, simplifie tes refactors

Pourquoi tu dois t'y intéresser maintenant

Depuis sa sortie en 2011, Dart a beaucoup évolué : l'async/await en 2017, les null-safe types en 2021, les records et sealed classes au printemps 2024… et, avec Dart 3, le pattern matching est enfin de la partie (Dart). Si tu connais Swift, Kotlin ou TypeScript, tu connais déjà la puissance qu'apporte un switch exhaustif ; si tu utilises Flutter, tu sais à quel point un widget se retrouve vite noyé sous des if/else imbriqués pour gérer les multiples états d'un écran.

Bonne nouvelle : tu n'as plus à choisir entre lisibilité et performance. Le pattern matching te permet de décrire la forme d'une donnée (sa shape) plutôt que la succession d'étapes pour l'analyser. Résultat :

  • moins de logique glue (as, cast, nullable checks),
  • un compilateur ravi qui t'avertit à la moindre case manquante,
  • des PR plus courtes et un onboarding junior plus rapide.

Dans cet article, je te montre comment le pattern matching résout des problèmes réels : parsing JSON, gestion d'états UI, validation de formulaires, et même construction de DSL maison.


1. Comprendre la philosophie : « Show me your shape »

Les creators de Dart n'ont pas copié/collé la feature de Swift ; ils l'ont pensée pour s'intégrer aux autres nouveautés de Dart 3 :

  1. Records : des tuples typés et nommés.
  2. Sealed classes : un ensemble fermé d'implémentations.
  3. Pattern matching : la glue qui relie les deux – tu peux matcher un record, un list literal, un map et une sealed hierarchy.

Le slogan interne était : « If you can read it, you can deconstruct it », d'après les notes de la team ; concrètement, toute expression littérale devient aussi un pattern (Dart). Ça rend la courbe d'apprentissage super douce : tu commences par matcher un 42 littéral, tu termines par démonter un JSON imbriqué en une seule ligne!


2. Mise en place rapide

Active la version 3.0 dans ton pubspec.yaml :

environment:
  sdk: ">=3.0.0 <4.0.0"

Puis, dans VS Code ou IntelliJ, assure-toi d'avoir le Dart SDK ≥ 3.0. Tout le reste est pur langage – aucun package externe requis.


3. Constant patterns — le Hello World du match

Cas d'usage : remplacer un enum verbose

Avant :

switch (connectionState) {
  case ConnectionState.idle:
    renderIdle();
    break;
  case ConnectionState.loading:
    renderSpinner();
    break;
  // …
}

Après (break implicite, exhaustivité vérifiée) :

switch (connectionState) {
  case ConnectionState.idle:
    renderIdle();
  case ConnectionState.loading:
    renderSpinner();
  // pas besoin de default si enum complet
}

À retenir : si l'énum change (par exemple avec l'ajout de la valeur error), le compilateur hurle tant que tu n'as pas ajouté la nouvelle clause. Un garde-fou gratuit !


4. Relational patterns — exprime tes plages numériques

Cas d'usage : notation d'examen

String mention(int score) => switch (score) {
  >= 17 => '🌟 Félicitations !',
  >= 14 && < 17 => 'Très bien',
  >= 10 && < 14 => 'Peut mieux faire',
  < 10 => 'Rattrapages',
};
  • Tu lis la règle au lieu du code.
  • Les bornes ne se chevauchent plus : le premier match s'exécute et sort.

Sous le capot, le compilateur génère des tests ordonnés pour éviter dupliquer les comparaisons. Rien à configurer.


5. Guards (when) — ajoute un twist logique

Cas d'usage : tarification dynamique

double priceFor(Order o) => switch (o) {
  Order(total: var t) when t > 100 => t * 0.9, // 10 % de remise
  Order(isVip: true) => o.total * 0.95,        // VIP
  _ => o.total,
};

Le guard permet de garder le pattern lisible (Order(total: var t)) et de déléguer la condition spécifique (when t > 100). Ça évite les case _ trop fourre-tout.


6. List patterns — manipule les tableaux comme en FP

Cas d'usage : extraire head et tail

void headTail<T>(List<T> items) => switch (items) {
  [var head, ... var tail] => print('head: $head, tail: $tail'),
  [] => print('Liste vide'),
};
  • ... var tail s'appelle un rest pattern : il capture le reste sans allocation supplémentaire.
  • [_, _, third] saute les deux premiers éléments ; l'underscore est un wildcard (pas de binding).

Variante Flutter : comprimer un breadcrumb

Widget breadcrumb(List<String> path) => switch (path) {
  [var first, ..., var last] =>
      Text('$first › … › $last', overflow: TextOverflow.ellipsis),
  [var single] => Text(single),
  _ => const SizedBox.shrink(),
};

Un rendu UX-friendly sans if ni boucle.


7. Map patterns — dis bye bye aux cast JSON

Le parsing JSON était l'un des scénarios phares dans la proposition initiale de la feature (Medium).

Cas d'usage : routing API

switch (request) {
  case {'method': 'GET', 'path': '/users', 'query': {'id': var id}}:
    return getUser(id);
  case {'method': 'POST', 'path': '/users', 'body': {'name': var n, 'age': var a}}:
    return createUser(n, a);
  default:
    return Response.notFound();
}

Points clés :

  1. Les clés manquantes font échouer le match : tu ne risques pas le NoSuchMethodError.
  2. Les variables (id, n, a) sont typées : si age n'est pas int, le pattern ne match pas.

8. Record patterns — bye bye Tuple2

Les records te permettent de retourner plusieurs valeurs sans créer de classe. Avec les patterns, tu peux les déstructurer côté appelant.

(String, int) fetchUser() => ('Alice', 30);

void greet() {
  switch (fetchUser()) {
    case ('Alice', var age) when age >= 30:
      print('Welcome back, Alice 💖');
    case (var name, var age):
      print('Hello $name, you are $age');
  }
}

Imagine la même logique avant : il te fallait soit une classe User, soit du as. Là, tu restes en code light. Un vrai gain pour les micro-fonctions utilitaires (Dart).


9. Object patterns & sealed classes — l'arme fatale pour tes states

Scénario Flutter : Gestion d'état d'écran

sealed class ScreenState {}

class Loading extends ScreenState {}
class Loaded extends ScreenState { Loaded(this.data); final List<Item> data; }
class Error extends ScreenState { Error(this.message); final String message; }

Widget buildScreen(ScreenState state) => switch (state) {
  Loading() => const CircularProgressIndicator(),
  Loaded(:var data) => ListView(children: data.map(Text.new).toList()),
  Error(:var message) => Center(child: Text('❌ $error')),
};
  • Exhaustivité : si demain tu ajoutes un état Empty, l'IDE te le signale.
  • Lecture instantanée : chaque case ressemble… au widget qu'il renvoie.

Astuce perf : couplé à flutter_hooks ou riverpod, tu peux isoler chaque sous-arbre pour un rebuild minimal.


10. DSL maison — construis tes propres mini-langages

Le pattern matching ouvre la porte aux Domain Specific Languages lisibles. Exemple : un moteur de règles de validation formidablement… compact.

typedef Rule = bool Function(dynamic value);

Rule isEmail    = (v) => v case String(:var s) && s.contains('@');
Rule minLength(int n) => (v) => v case String(:var s) && s.length >= n;

bool validate(dynamic value, List<Rule> rules) =>
    rules.every((r) => r(value));

void main() {
  print(validate('john@acme.io', [isEmail, minLength(5)])); // true
}

Ici, les expressions patterns se cachent dans une closure anonyme : pas besoin de switch formel.


11. Pièges & bonnes pratiques (màj)

PiègeComment l'éviter
case [_, _, _] sur liste non fixéeUtilise [... _] pour accepter longueur variable, sinon un default.
Oublier == vs = dans les guardsRappelle-toi que le guard est un booléen pur.
Pattern trop général en premierOrdonne tes case du plus spécifique au plus large.
Migrer un vieux switch sans testsCommence par copier les tests avant refacto ; le compilateur ne détecte pas la logique métier.

12. Migration d'un code legacy : étude de cas

Situation initiale

// 80 lignes, 6 niveaux de if/else
if (json['type'] == 'movie') {
  if (json['year'] > 2000) { … }
  else { … }
} else if (json['type'] == 'series') {
  // …
}

Étapes de migration

  1. Lister les valeurs possibles (movie, series, etc.).
  2. Définir un switch (json) avec un Map pattern par type.
  3. Éliminer progressivement les if internes en sub-patterns (>=, [_, var lead, ...]).
  4. Couvrir le cas inattendu avec default.
  5. Exécuter les tests – tous passent ? commit.

Résultat : 80 → 28 lignes, lecture instantanée, performance identique selon dart compile exe.


13. Conclusion — le moment d'agir

Le pattern matching coche la sainte trinité : lisible, sûr, rapide. Que tu travailles sur une appli Flutter, un micro-service dart_frog ou un CLI en pur Dart, tu y gagneras :

  • 20–40 % de lignes en moins sur la logique conditionnelle dans mes projets clients.
  • Bugs d'état réduits : l'exhaustivité compile-time remplace les checklists manuelles.
  • Onboarding accéléré : un junior comprend un switch en 5 s, pas un if monstre.

Références

  • Migration guide Dart 3 — aperçu des ruptures (Dart)
  • Patterns — documentation officielle Dart (Dart)
  • Article de terrain — « Conditional pattern matching in Dart 3 » par Stephan Mantel (Medium)
  • Pattern types — référence détaillée (Dart)

Tags

  • flutter
  • dart
  • pattern matching
  • mobile

Cet article à été posté le