DSL in TypeScript: Erstellen Sie Ihre eigene Programmiersprache an einem Abend

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Warum Ihr Projekt eine eigene Sprache verdient?

\n\n

Stellen Sie sich vor: Sie schreiben Code zur Berechnung von Versicherungsprämien und sehen anstelle von Dutzenden verschachtelter if-else-Anweisungen und magischen Zahlen eine prägnante Konstruktion:

\n\n
const rule = new InsuranceRule()\n  .when(age > 25)\n  .and(experience > 2)\n  .then(premium = base * 0.8);
\n\n

Das ist keine Magie. Es ist ein DSL (Domain Specific Language) – eine domänenspezifische Sprache, eingebettet in TypeScript. In diesem Artikel werden wir uns ansehen, wie man solche Sprachen erstellt, damit Ihr Code die Sprache des Geschäfts spricht, nicht die der Maschinen.

\n\n

Was ist ein DSL und warum wird es benötigt?

\n\n

Ein DSL ist eine Sprache, die zur Lösung von Aufgaben einer bestimmten Fachdomäne entwickelt wurde. Im Gegensatz zu allgemeinen Sprachen (Java, C#, TypeScript) versucht ein DSL nicht, universell zu sein. Es opfert Flexibilität zugunsten der Ausdruckskraft.

\n\n

Wann sollte man ein DSL verwenden:

\n
    \n
  • Die Geschäftslogik ändert sich häufig und soll von Analysten bearbeitet werden können
  • \n
  • Im Projekt gibt es viele sich wiederholende Konfigurationsmuster
  • \n
  • Komplexe Algorithmen müssen vom restlichen Code isoliert werden
  • \n
  • Eine Datenvalidierung auf Grammatikebene ist erforderlich
  • \n
\n\n

TypeScript ist die ideale Wahl für die Erstellung von DSLs. Statische Typisierung, ein leistungsstarkes Typsystem und die Möglichkeit, in JavaScript zu kompilieren, machen es zu einem flexiblen Werkzeug für die Konstruktion sowohl interner als auch externer Sprachen.

\n\n

Interne DSLs: Fluent API und Methodenverkettung

\n\n

Der einfachste Weg, ein DSL in TypeScript zu erstellen, ist die Verwendung eines Fluent Interface. Dabei gibt jede Methode das Objekt selbst zurück, was die Verkettung von Aufrufen ermöglicht.

\n\n

Beispiel: SQL-Abfrage-Builder

\n\n
class 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// Verwendung\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\n

Dies ist ein klassisches internes DSL in TypeScript. Es benötigt keinen separaten Parser und funktioniert innerhalb des TypeScript-Codes. Der Hauptvorteil ist die vollständige Unterstützung für Autovervollständigung und Typisierung.

\n\n

Komplexer: Typisiertes DSL für Geschäftsregeln

\n\n

Fügen wir eine strenge Typisierung mit Generics und Union-Typen hinzu:

\n\n
type 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// Datentyp für die Prüfung\ninterface UserData                

Blogs

Buchempfehlungen