Pattern matching Dart 3 : booste ta logique, simplifie tes refactors
Sommaire
- Pourquoi tu dois t'y intéresser maintenant
- 1. Comprendre la philosophie : « Show me your shape »
- 2. Mise en place rapide
- 3. Constant patterns — le Hello World du match
- 4. Relational patterns — exprime tes plages numériques
- 5. Guards (
when
) — ajoute un twist logique - 6. List patterns — manipule les tableaux comme en FP
- 7. Map patterns — dis bye bye aux cast JSON
- 8. Record patterns — bye bye
Tuple2
- 9. Object patterns & sealed classes — l'arme fatale pour tes states
- 10. DSL maison — construis tes propres mini-langages
- 11. Pièges & bonnes pratiques (màj)
- 12. Migration d'un code legacy : étude de cas
- 13. Conclusion — le moment d'agir
- Références
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 :
- Records : des tuples typés et nommés.
- Sealed classes : un ensemble fermé d'implémentations.
- 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 :
- Les clés manquantes font échouer le match : tu ne risques pas le
NoSuchMethodError
. - Les variables (
id
,n
,a
) sont typées : siage
n'est pasint
, 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
ouriverpod
, 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ège | Comment l'éviter |
---|---|
case [_, _, _] sur liste non fixée | Utilise [... _] pour accepter longueur variable, sinon un default . |
Oublier == vs = dans les guards | Rappelle-toi que le guard est un booléen pur. |
Pattern trop général en premier | Ordonne tes case du plus spécifique au plus large. |
Migrer un vieux switch sans tests | Commence 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
- Lister les valeurs possibles (
movie
,series
, etc.). - Définir un
switch (json)
avec un Map pattern par type. - Éliminer progressivement les
if
internes en sub-patterns (>=
,[_, var lead, ...]
). - Couvrir le cas inattendu avec
default
. - 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 unif
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)