Por que seu projeto merece sua própria linguagem?
\n\nImagine: você escreve código para calcular prêmios de seguro e, em vez de dezenas de if-else aninhados e números mágicos, vê uma construção concisa:
const rule = new InsuranceRule()\n .when(age > 25)\n .and(experience > 2)\n .then(premium = base * 0.8);\n\nIsso não é mágica. Isso é um DSL (Domain Specific Language) — uma linguagem específica de domínio, incorporada ao TypeScript. Neste artigo, vamos explorar como criar essas linguagens para que seu código fale a linguagem do negócio, e não a linguagem das máquinas.
\n\nO que é DSL e por que você precisa dela?
\n\nDSL é uma linguagem criada para resolver problemas de um domínio específico. Ao contrário das linguagens de propósito geral (Java, C#, TypeScript), a DSL não tenta ser universal. Ela sacrifica flexibilidade em prol da expressividade.
\n\nQuando usar DSL:
\n- \n
- A lógica de negócios muda com frequência e precisa ser editada por analistas \n
- O projeto tem muitos padrões de configuração repetitivos \n
- É necessário isolar algoritmos complexos do restante do código \n
- É necessária validação de dados no nível da gramática \n
TypeScript é a escolha ideal para criar DSLs. Tipagem estática, um sistema de tipos poderoso e a capacidade de compilar para JavaScript o tornam uma ferramenta flexível para construir linguagens internas e externas.
\n\nDSLs internas: fluent API e encadeamento de chamadas
\n\nA maneira mais simples de criar uma DSL em TypeScript é usar uma Fluent Interface (interface fluente). Isso ocorre quando cada método retorna o próprio objeto, permitindo construir cadeias de chamadas.
\n\nExemplo: construtor de consultas 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// Uso\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\nEste é um DSL interno clássico em TypeScript. Ele não requer um parser separado e funciona dentro do código TypeScript. A principal vantagem é o suporte completo a autocompletar e tipagem.
\n\nComplexificando: DSL tipado para regras de negócio
\n\nVamos adicionar tipagem estrita com genéricos e tipos union:
\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// Tipo de dados para verificação\ninterface UserData