Pourquoi votre projet mérite son propre langage ?
\n\nImaginez : vous écrivez du code pour calculer des primes d'assurance, et au lieu de dizaines de if-else imbriqués et de nombres magiques, vous voyez une construction concise :
const rule = new InsuranceRule()\n .when(age > 25)\n .and(experience > 2)\n .then(premium = base * 0.8);\n\nCe n'est pas de la magie. C'est un DSL (Domain Specific Language) — un langage dédié, intégré dans TypeScript. Dans cet article, nous allons voir comment créer de tels langages pour que votre code parle le langage du métier, et non celui des machines.
\n\nQu'est-ce qu'un DSL et pourquoi en a-t-on besoin ?
\n\nUn DSL est un langage créé pour résoudre les problèmes d'un seul domaine métier spécifique. Contrairement aux langages généralistes (Java, C#, TypeScript), un DSL ne cherche pas à être universel. Il sacrifie la flexibilité au profit de l'expressivité.
\n\nQuand utiliser un DSL :
\n- \n
- La logique métier change souvent et doit pouvoir être modifiée par des analystes \n
- Le projet comporte de nombreux motifs de configuration répétitifs \n
- Il faut isoler des algorithmes complexes du reste du code \n
- Une validation des données au niveau de la grammaire est nécessaire \n
TypeScript est le choix idéal pour créer des DSL. Le typage statique, le système de types puissant et la possibilité de compiler en JavaScript en font un outil flexible pour construire à la fois des langages internes et externes.
\n\nDSL internes : API fluide et chaînage d'appels
\n\nLa façon la plus simple de créer un DSL en TypeScript est d'utiliser une Interface Fluent. Chaque méthode retourne l'objet lui-même, permettant de construire des chaînes d'appels.
\n\nExemple : constructeur de requêtes SQL
\n\nclass QueryBuilder {\n private table: string = '';\n private conditions: string[] = [];\n private selectedFields: string[] = ['*'];\n\n select(...fields: string[]): this {\n this.selectedFields = fields.length ? fields : ['*'];\n return this;\n }\n\n from(table: string): this {\n this.table = table;\n return this;\n }\n\n where(condition: string): this {\n this.conditions.push(condition);\n return this;\n }\n\n and(condition: string): this {\n return this.where(condition);\n }\n\n build(): string {\n let sql = `SELECT ${this.selectedFields.join(', ')} FROM ${this.table}`;\n if (this.conditions.length) {\n sql += ` WHERE ${this.conditions.join(' AND ')}`;\n }\n return sql;\n }\n}\n\n// Utilisation\nconst query = new QueryBuilder()\n .select('name', 'age')\n .from('users')\n .where('age > 18')\n .and('status = "active"')\n .build();\n\nconsole.log(query);\n// SELECT name, age FROM users WHERE age > 18 AND status = "active"\n\nC'est un DSL interne en TypeScript classique. Il ne nécessite pas d'analyseur syntaxique séparé et fonctionne à l'intérieur du code TypeScript. Le principal avantage est le support complet de l'autocomplétion et du typage.
\n\nComplexifions : DSL typé pour les règles métier
\n\nAjoutons un typage strict avec des génériques et des types unions :
\n\ntype ComparisonOperator = '>' | '<' | '=' | '!=';\n\nclass RuleBuilder<T extends Record<string, any>> {\n private conditions: ((data: T) => boolean)[] = [];\n\n when<K extends keyof T>(field: K, op: ComparisonOperator, value: T[K]): this {\n this.conditions.push((data: T) => {\n switch (op) {\n case '>': return data[field] > value;\n case '<': return data[field] < value;\n case '=': return data[field] === value;\n case '!=': return data[field] !== value;\n }\n });\n return this;\n }\n\n and<K extends keyof T>(field: K, op: ComparisonOperator, value: T[K]): this {\n return this.when(field, op, value);\n }\n\n evaluate(data: T): boolean {\n return this.conditions.every(c => c(data));\n }\n}\n\n// Type de données pour la vérification\ninterface UserData