Parser combinators en TypeScript, paso a paso
@programacionLo esencial
- Un parser combinator es una función que recibe un string y devuelve un valor junto con el resto sin consumir.
- TypeScript tipa cada parser como Parser<T>: una función de string a la tupla [T, string].
- pSeq, pMap y pOr combinan parsers pequeños en parsers complejos sin generar código ni gramáticas formales.
- El backtracking de pOr con try/catch puede volverse exponencial en gramáticas grandes con muchas alternativas anidadas.
- El artículo que popularizó el patrón en TypeScript lo publicó T3 el 17 de octubre de 2026.
TypeScript te permite convertir el string "18.2.7-beta" en un objeto { major: 18, minor: 2, patch: 7 } con apenas cinco funciones chicas, sin tocar una expresión regular compleja ni instalar un generador de parsers.
¿Qué problema resuelven los parser combinators?
Parsear es convertir texto plano en datos estructurados. Un regex sirve para extraer un dato puntual, como un número o una fecha, pero se vuelve ilegible cuando el texto tiene estructura anidada: una lista de pares clave-valor, una expresión con paréntesis o un archivo de configuración con secciones.
Herramientas como tree-sitter manejan esa anidación, pero exigen escribir una gramática formal y compilar un parser completo, algo excesivo para un script de 50 líneas. Los parser combinators ocupan el punto medio: se construyen piezas pequeñas y se combinan con funciones de orden superior.
El artículo que popularizó este enfoque en TypeScript lo publicó T3 el 17 de octubre de 2026, usando como ejemplo transformar la salida de ping en un objeto estructurado.
El tipo Parser<T>: la pieza mínima
La pieza mínima es una función con esta firma: type Parser<T> = (input: string) => [T, string]. Recibe el string que falta por consumir y devuelve una tupla con el valor extraído y el resto sin tocar.
Cualquier función que respete esa firma es un parser válido, sin heredar de una clase ni registrarse en ningún framework. Si necesitás repasar la sintaxis de genéricos, la referencia oficial está en los docs de TypeScript.
type Parser<T> = (input: string) => [T, string];
const pInteger: Parser<number> = (input) => {
const match = input.match(/^\d+/);
if (match === null) throw new Error("se esperaba un número");
const digits = match[0];
return [Number(digits), input.slice(digits.length)];
};pInteger usa una expresión regular por dentro, y eso está permitido: un parser combinator no prohíbe regex, solo evita que sea la única herramienta. Si no encuentra dígitos al inicio, lanza un error, y ese error es la señal que usan los combinadores de más arriba para decidir si probar otra alternativa.
Encadenar parsers con pSeq
pSeq ejecuta una lista de parsers en orden, pasándole a cada uno el resto que dejó el anterior. El resultado es una tupla con un valor por cada parser, en el mismo orden en que se declararon.
function pSeq<T extends unknown[]>(
...parsers: { [K in keyof T]: Parser<T[K]> }
): Parser<T> {
return (input) => {
const resultados: unknown[] = [];
let resto = input;
for (const parser of parsers) {
const [valor, nuevoResto] = parser(resto);
resultados.push(valor);
resto = nuevoResto;
}
return [resultados as T, resto];
};
}Con pSeq ya se puede describir una gramática completa como una lista de pasos, sin escribir un switch gigante ni un autómata a mano. Cada paso sigue siendo una función de tres o cuatro líneas, fácil de probar por separado.
Transformar resultados con pMap
pSeq devuelve tuplas, útiles pero poco legibles cuando hay varios campos. pMap aplica una función de transformación al valor que devuelve un parser, sin tocar el resto del string.
const pLiteral = (texto: string): Parser<string> => (input) => {
if (!input.startsWith(texto)) throw new Error(`se esperaba "${texto}"`);
return [texto, input.slice(texto.length)];
};
const pMap = <T, U>(parser: Parser<T>, fn: (valor: T) => U): Parser<U> =>
(input) => {
const [valor, resto] = parser(input);
return [fn(valor), resto];
};
const pActivo: Parser<true> = pMap(pLiteral("activo"), () => true as const);
pActivo("activo=si");
// [true, "=si"]pLiteral generaliza la idea de pHello del artículo original a cualquier string fijo, no solo a una palabra concreta. Combinado con pMap, convierte el texto plano "activo" en un boolean real de TypeScript, listo para usarse en el resto del programa.
Elegir alternativas con pOr
Hasta ahora todos los combinadores asumen que el parser va a tener éxito. pOr agrega la otra mitad: probar varias alternativas en orden y quedarse con la primera que no lance error.
const pOr = <T,>(...parsers: Parser<T>[]): Parser<T> => (input) => {
for (const parser of parsers) {
try {
return parser(input);
} catch {
continue;
}
}
throw new Error("ninguna alternativa coincidió");
};
const pBooleano: Parser<boolean> = pOr(
pMap(pLiteral("activo"), () => true),
pMap(pLiteral("inactivo"), () => false)
);
pBooleano("inactivo,siguiente");
// [false, ",siguiente"]Este es el mecanismo de backtracking más simple que existe. Si pLiteral("activo") falla, pOr descarta ese intento y prueba pLiteral("inactivo") sobre el input original, no sobre el resto que dejó el intento fallido.
Ejemplo práctico: parsear una versión semántica
Con pInteger, pLiteral, pSeq y pMap ya alcanza para resolver un caso real: convertir una versión semántica como "18.2.7-beta" en un objeto tipado, sin librerías externas.
const pPunto = pLiteral(".");
const pVersion: Parser<{ major: number; minor: number; patch: number }> =
pMap(
pSeq(pInteger, pPunto, pInteger, pPunto, pInteger),
([major, , minor, , patch]) => ({ major, minor, patch })
);
pVersion("18.2.7-beta");
// [{ major: 18, minor: 2, patch: 7 }, "-beta"]El guion y "beta" quedan en el resto sin consumir porque la gramática solo describió major.minor.patch. Agregar ese sufijo es simplemente escribir un pSeq más largo con un parser nuevo para la etiqueta.
Cuándo no conviene usar parser combinators
El backtracking con try/catch de pOr es fácil de entender, pero cuesta. Si la gramática tiene muchas alternativas anidadas, el mismo tramo de texto se puede reanalizar varias veces, y el tiempo de ejecución crece rápido en el peor caso.
Para gramáticas grandes de verdad, como un lenguaje de programación completo, conviene memoizar resultados por posición de entrada (la técnica se llama packrat parsing) o directamente usar un generador de parsers. Además, los errores de esta versión mínima son genéricos ("se esperaba un número"): un parser de producción necesita guardar la posición exacta del fallo para dar mensajes útiles, algo que ninguno de los ejemplos de arriba hace todavía.
Por qué importa este patrón en 2026
El enfoque no es nuevo, nace de Haskell en los años noventa, pero en TypeScript resuelve un problema concreto: parsear la salida de herramientas de línea de comandos, archivos de configuración caseros o mini-lenguajes de dominio sin arrastrar una dependencia pesada.
El ejemplo original de T3 parte de la salida de ping y llega a un objeto con header, packets y statistics usando exactamente los mismos combinadores de este artículo: pSeq, pMap, pOr y una variante de pInteger para números con decimales.
Conclusión
Un parser combinator es, en el fondo, una función con una firma fija y cuatro o cinco combinadores genéricos para componerlas. Esa simplicidad es la razón para conocerlos antes de instalar tree-sitter o escribir el enésimo regex ilegible.
📖 Versión extendida con más detalle: https://elsolitario.org/2026/10/18/parser-combinators-en-typescript-explicados/?utm_source=telegraph&utm_medium=instant_view&utm_campaign=programacion