¿Por qué tu proyecto merece su propio lenguaje?
\n\nImagina: estás escribiendo código para calcular primas de seguros y, en lugar de docenas de if-else anidados y números mágicos, ves una construcción concisa:
const rule = new InsuranceRule()\n .when(age > 25)\n .and(experience > 2)\n .then(premium = base * 0.8);\n\nEsto no es magia. Es un DSL (Domain Specific Language) — un lenguaje específico del dominio, integrado en TypeScript. En este artículo, analizaremos cómo crear estos lenguajes para que tu código hable el lenguaje del negocio, no el de las máquinas.
\n\n¿Qué es un DSL y para qué sirve?
\n\nUn DSL es un lenguaje creado para resolver problemas de un dominio específico. A diferencia de los lenguajes de propósito general (Java, C#, TypeScript), un DSL no intenta ser universal. Sacrifica flexibilidad por expresividad.
\n\n¿Cuándo usar un DSL?
\n- \n
- La lógica de negocio cambia a menudo y necesita ser modificada por analistas \n
- El proyecto tiene muchos patrones de configuración repetitivos \n
- Se necesita aislar algoritmos complejos del resto del código \n
- Se requiere validación de datos a nivel de gramática \n
TypeScript es la elección ideal para crear DSL. Su tipado estático, su potente sistema de tipos y la capacidad de compilar a JavaScript lo convierten en una herramienta flexible para construir tanto lenguajes internos como externos.
\n\nDSL internos: fluent API y cadenas de llamadas
\n\nLa forma más sencilla de crear un DSL en TypeScript es usar una Fluent Interface (interfaz fluida). Esto ocurre cuando cada método devuelve el propio objeto, permitiendo construir cadenas de llamadas.
\n\nEjemplo: constructor 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 es un DSL interno clásico en TypeScript. No requiere un analizador separado y funciona dentro del código TypeScript. La principal ventaja es el soporte completo de autocompletado y tipado.
\n\nComplicándolo: DSL tipado para reglas de negocio
\n\nAñadamos tipado estricto con genéricos y tipos unión:
\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 datos para la validación\ninterface UserData