Factories — Your Superpower in Typescript

Factories — Your Superpower in Typescript

There is one tool that has completely changed the way I write code. Factories. And even though I am German, I don’t mean the one they are building BMW’s in.

It doesn’t matter whether you are a frontend or backend developer, whether you dig object-oriented programming or not. You will think differently about software structure and write 10x better code after you learned about this magical technique.

The Problem

I bet you have encountered this problem before. You write an innocent function that converts an object before you send it off on an API request or render it in a component.

Your app grows and suddenly a second object appears that needs a slightly different conversion. No problem. You add an if statement and go back to work.

A third object asks yet for another conversion. Ok, you got this. You add a Switch statement. Before you know it you have a massive Switch statement not only in this place, but in ten others.

When you need to make an update to an object you suddenly need to change your code everywhere. Welcome to Spaghetti land.

The Solution

Let’s explore a solution to this problem by looking at a block editor, similar to the ones that Medium or Notion use.

A block editor consists of different blocks of content. They have some properties in common, but might also have additional attributes that are unique to them.

Our editor has three different blocks — paragraph, heading and image. I know. Quite underwhelming. But as we will see later, it will be easy to add blocks to the editor without changing a lot of our code.

We define our first block as a simple object with a Typescript interface:

interface Paragraph {
	id: string;
	text: string;
}

Now we want to bring this paragraph block on the screen. We create a render function that returns the paragraph as an HTML element.

export function render(block: Paragraph): Html {
	const { id, text } = block;
	return `<p data-id=${id}>${text}</p>`;
}

Great! We are getting a paragraph rendered. Now let’s implement our heading block as well.

interface Heading {
	id: string;
	level: 1 | 2 | 3 | 4 | 5 | 6;
	text: string;
}

Not much has changed except for that we need an additional level to indicate which heading we want to render.

We can now update our render function to also handle the Heading block:

export function render(block: Paragraph | Heading): Html {
	const { id, text } = block;
	if (block.level) {
		return `<h${block.level} data-id=${id}>block.text</h${block.level}>`
	}
	return `<p data-id=${id}>${text}</p>`;
}

That code already looks a bit sketchy. What if we have another block later on that has also a level property. It’s also not immediately clear to anyone finding this function in the codebase what is going on.

But we soldier on. We have one block to go for our editor — the image.

interface Image {
	id: string;
	url: string;
	caption?: string;
}

Now we suddenly don’t have a text field anymore. And we have two additional ones. Let’s introduce a new common field to our blocks — a type. That way we can at least tell what we are dealing with.

enum BlockType {
	paragraph = 'paragraph',
	heading = 'heading',
	image = 'image'
}

interface Paragraph {
	id: string;
	type: BlockType.paragraph;
	text: string;
}

interface Heading {
	id: string;
	type: BlockType.heading;
	level: 1 | 2 | 3 | 4 | 5 | 6;
	text: string;
}

interface Image {
	id: string;
	type: BlockType.image;
	url: string;
	caption?: string;
}

Great. That gives us a better way to structure our rendering function:

export function render(block: Paragraph | Heading | Image): Html {
	switch (block.type) {
		case BlockType.paragraph:
			return `<p data-id=${block.id}>${block.text}</p>`;
		case BlockType.heading:
			return `<h${block.level} data-id=${block.id}>block.text</h${block.level}>`
		case BlockType.image:
			return `
				<figure data-id=${block.id}>
  					<img src=${block.url} alt=${block.caption} style="width:100%">
	 				<figcaption>${block.caption}</figcaption>
				</figure>`;
	}
	
	
}

This code is already much better. We can check for the type of the block and can then confidently render the correct element. But our function is already growing, and we only have three blocks implemented so far.

Is there a better way to let each block decide themselves how they get rendered?

There is and this way leads us to a factory.

Leveraging factories

A factory is a creational pattern in object-oriented programming that creates objects. The advantage of using a factory in our blocks example is that we don’t have to know upfront exactly which object we are going to create.

First, we are going to write a Block base class that we can later reference in each individual block. Let’s make this an abstract class:

export interface BlockProps {
	id: string;
	type: BlockType;
}

export abstract class Block {
	constructor(props: BlockProps) {
		this.id = props.id;
		this.type = props.type;
	}
}

Now we can implement a class for each of our three blocks. We keep it simply for now and create them all simply with the constructor method.

interface ParagraphProps extends BlockProps {
	text: string;
}

export class Paragraph extends Block {
	constructor(props: ParagraphProps) {
		super(props)
		this.text = props.text;
	}
}
interface HeadingProps extends BlockProps {
	text: string;
	level: 1 | 2 | 3 | 4 | 5 | 6;
}

export class Heading extends Block {
	constructor(props: HeadingProps) {
		super(props)
		this.text = props.text;
		this.level = props.level;
	}
}
interface ImageProps extends BlockProps {
	url: string;
	caption?: string;
}

export class Image extends Block {
	constructor(props: ImageProps) {
		super(props)
		this.url = props.url;
		this.caption = props.caption
	}
}

Now we have written a lot of code. And you might be wondering: what is the point of all this class theater? Stay with me.

We have built the foundation to create all of our blocks. Now we also want every block to implement a renderHtml method that renders the block. First we create an abstract field in our Block base class to type-check that every class implements this:

export interface BlockProps {
	id: string;
	type: BlockType;
}

export abstract class Block {
	constructor(props: BlockProps) {
		this.id = props.id;
		this.type = props.type;
	}

	abstract renderHtml(): Html;
}

And now we can use the render implementation we wrote before in each of the blocks:

interface ParagraphProps extends BlockProps {
	text: string;
}

export class Paragraph extends Block {
	public constructor(props: ParagraphProps) {
		super(props)
		this.text = props.text;
	}

	public renderHtml() {
		return `<p data-id=${this.id}>${this.text}</p>`
	}
}
interface HeadingProps extends BlockProps {
	text: string;
	level: 1 | 2 | 3 | 4 | 5 | 6;
}

export class Heading extends Block {
	constructor(props: HeadingProps) {
		super(props)
		this.text = props.text;
		this.level = props.level;
	}

	public renderHtml() {
		return `<h${this.level} data-id=${this.id}>block.text</h${this.level}>`
	}
}
interface ImageProps extends BlockProps {
	url: string;
	caption?: string;
}

export class Image extends Block {
	constructor(props: ImageProps) {
		super(props)
		this.url = props.url;
		this.caption = props.caption
	}

	public renderHtml() {
	    return `
            <figure data-id=${block.id}>
                <img src=${block.url} alt=${block.caption} style="width:100%">
                <figcaption>${block.caption}</figcaption>
            </figure>`;
	}
}

But how do we know now which block to use in our render function when rendering the blocks to the page? This is where our factory comes in. The factory is basically just a function that will, depending on the type of block, render the correct object.

I prefer to write the function as a static method inside a class, but you could just as well use a plain function. Let’s have a look:

export class BlockFactory {
	public static create(props: BlockProps): Block {
		switch(props.type) {
			case BlockType.paragraph:
				return new Paragraph(props);
			case BlockType.heading:
				return new Heading(props);
			case BlockType.image:
				return new Image(props);
			default:
				throw new Error('Unknown block type');
		}
	}
}

And our render function now looks like this:

export function render(blockProps: BlockProps): Html {
		const block = BlockFactory.create(blockProps);
		return block.render();
}

Much cleaner right?

I know what you are thinking. We just wrote way more code to get the same result. This switch statement was more or less in the render function before.

But think about all the places in your app where you will need to switch through the different block types you have. We can not only include rendering a block, but we might add methods like toJson, toDb and many more.

Validation

You need to be extra careful with validating your objects when using factories. Since you can’t type-check them statically, you might run into cryptic errors at runtime.

I recommend creating a static create method on each block that checks if all required properties are defined and of the right type. Writing tests is also a great way to make sure that your factory behaves the way you want it to.

As your project keeps growing, you will realize how easy it is to add new functionality. You won’t need to change the code in many places at once, but you can define the behavior on your objects and load them as needed.