The builder pattern in TypeScript is amazing. However, the way you use it in TypeScript is completely different to other languages. Typically, builders are used to add support for optional and default parameters in languages that don’t support them directly. TypeScript already supports optional and default parameters though - so what’s the point?

We can actually use builders as a workaround for other issues in the type system. In TypeScript, that means a way to enforce complex constraints like only allow sending an event if anyone is listening for that event. In this (long but beginner-friendly) post, we do an in-depth walkthrough of how to create a TypeScript builder class to enforce a complex constraint like that one.

We start by introducing a simple data processing task and discuss how it would be useful to create a generic version. That immediately gets too hard, so we introduce builders to save the day. Step-by-step, we analyse the problem and design a builder to solve the problem. We then discuss the pros and cons of that approach, and explore whether it was even necessary in the first place.

A Simple Task

Let’s imagine a basic data processing task:

This definitely warranted a 5000 word blog post

  1. Take an integer string as input
  2. Reverse it
  3. Parse it back into an integer
  4. Multiply it by 5
const input = "524";
const a = input.split("").reverse().join("");
const b = parseInt(input, 10);
const c = b * 5;

The rest of this blog post is dedicated to over-engineering that tiny bit of code. It’s clearly overkill in this case, but that’s inevitable when we use a simple example to demonstrate an advanced technique.

Making it Reusable

We saw how to do that task as a one-off, but what if we wanted it to be a configurable reusable function? We can define a function that takes a few config parameters:

Not that you'd ever really want to set the radix

function process(input: string, radix: number, multiplicand: number) {
  const a = input.split("").reverse().join("");
  const b = parseInt(input, radix);
  const c = b * multiplicand;
  return c;
}
process("524", 10, 5);

Often, it’s easier to take the config as a single object:

We're definitely doing it this way because it's intuitive and not because it makes the rest of the blog post simpler

function process(input: string, config: {radix: number, multiplicand: number}) {
  const a = input.split("").reverse().join("");
  const b = parseInt(input, config.radix);
  const c = b * config.multiplicand;
  return c;
}
process("524", {radix: 10, multiplicand: 5});

That’s useful as it allows the config to be loaded from a JSON file. Conveniently it also makes our job easier later on. What are the odds?

#swaterman #tech #typescript

TypeScript Builders: Improving your types one step at a time
1.10 GEEK