Parser combinators in TypeScript, shown by parsing real ping output

Regex gets unreadable on nested formats and tree-sitter is heavy. A short blog series builds a typed middle path from plain functions.

In brief

A parser here is a function from a string to a value and the unread remainder. Small ones chain into larger ones with pSeq, pMap and pRepeat, and the typed result is a tuple or object rather than a match array.

The finished example parses the header, per-packet lines and summary of a real ping run into one typed object.

A parser is a function that returns the rest

Parser combinators series image
Parser combinators series image

The article defines Parser<T> as a function from a string to [T, rest]. pInteger matches digits with a regex; pHello checks a literal prefix. Chaining means feeding each parser the leftover string of the previous one.

The generic pieces are small:

  • pSeq chains any number of parsers and collects results into a typed tuple, using TypeScript generics and variadic types.
  • pMap transforms a result, for example turning a tuple into a named object.
  • pRepeat applies a parser until it fails, like regex *.

Because every combinator takes parsers and returns a parser, grammars are built from nested functions. The worked example parses real ping output: header, per-packet lines and summary statistics, into one typed object. The author's argument is that this is easier to read, test and maintain than a large regex for nested formats.

The signature hides failure, and the limits went unmentioned

A commenter who argues elsewhere that parsers should return Either objected that the type promises a T for every input:

"…the function isn't well defined on all inputs."

The article's author, who replied in-thread and whose profile links t3.at, confirmed the gap:

"Since we're working in typescript a partial parser can always throw an exception (It's what the parsimmon library does aswell I believe). I decided not to get into those technicalities too much!"

A 52,000-karma regular wanted the costs stated up front:

"They should at least point out the limitations of the approach. For example that they cannot handle left-recursive grammars, and that they tend to backtrack a lot on ambiguous grammars leading to exponentially bad performance."

contificate, whose profile links compiler.club, named a benefit the series skips: continuation-passing parsers get streaming for free.

"So, many of these parsers work well on streams (e.g. TCP sockets) without manually maintaining a state machine - as you may do in C."

Ping output is flat and line-oriented, so it is a safe fit. Before using this on a recursive or ambiguous grammar, decide how failures surface (the article throws) and test the backtracking cost; when a regex passes about 100 characters on a flat format, this pattern is the one to try.

Get breakdowns like this every day

The top Hacker News stories, unpacked — by email, Telegram, or RSS.

Next story · 8 of 8 Read more
🚀Tech

A hobbyist 3D printer that positions to 1 micron and deposits 4-micron resin droplets

The µRepRap project wants micron-scale fabrication to be as accessible as RepRap made desktop 3D printing, using common parts and a UV LED under the bed.

★ 334 · 💬 54 · github.com · 2 min