At Applifting, we often strive to utilize our digital craftsmanship to create not just working code but good code. This is usually what brings happiness to our developers—the ability to make use of their deep knowledge and let their creativity solve problems while developing a good environment for others to work on the same code. We value a fair mix of maintainability and elegance. We prefer not to rely on tricks nor producing write-only code but finding clever ways to make development easier. This also extends beyond the confines of Applifting, as one of our tribes, DX Heroes, specifically focuses on improving developer experience.
We use a range of languages in our projects, but today, I want to talk about TypeScript. When I was offered to work in TypeScript, I immediately fell in love with its implementation of a type system. It made my head fill with thoughts of all the borderline insane types we could craft to make entire classes of errors a thing of the past. This comes at the cost of having to manage those type definitions, of course, but we still have tests. Strong types are complementary here, allowing tests to focus more on actual features and less on ensuring that we use our internal APIs correctly.
Creating a language parser
Having experienced Haskell at university, having written some template magicks in C++, and in general being interested in higher-order types, I felt more than comfortable experimenting in TypeScript. As I descended into the depths of development, I was tasked with producing a parser for a domain-specific language–Comlink–that we were also developing for our client Superface. This language was simple enough (and the project was lenient enough) to create our own recursive descent parser, to define a lexer and its tokens, and to then start defining syntax rules. Afterwards came rule combinators, conceptually simple "or", "and", "optional", "repeat", etc. As the complexity was rising, it became clear that it was imperative to create comprehensive typing for our code to keep our sanity at reasonable levels. And we got more focused tests as a bonus. It allowed us to handle local complexity better by constraining global complexity—by checking correct integration at the type level.
Consider the following type definitions, which imply a very simple toy language, with a construct like foo = 1 + bar - baz. We also assume equal operator precedence.