Entity factories

· erock's devlog

How I build functions to create new entities using typescript

Entity factories are functions that are used to create an object conforming to a specific typescript interface. These functions are tedious to write but offer a ton of benefits. I use them for virtually every entity I need to build and on every single project.

What's the problem? #

Let's say we have an entity called User. The user has a set of properties on it:

1interface User {
2  id: string;
3  name: string;
4  email: string;
5  verified: boolean;
6  admin: boolean;
7}

Okay now we want to create a user entity in our project, the default way to do that would be something like:

1const user: User = {
2  id: createId(),
3  name: "user name",
4  email: "user@name.com",
5  verified: false,
6  admin: false,
7};

Doing this once seems straight-forward, but what about 10 times? 30 times? What about when you want to write some unit tests and you have to create a user entity a bunch of times?

This is where the problem lies. It seems easy to just build the entity everytime you need it but other problems arise: what happens when you need to refactor the user object to add or remove a property? Now you have to traverse the entire codebase to make the change to all of the objects.

What's the solution? #

If we take the same User entity and create a function that builds the entity for us, we can reuse it everywhere:

 1const defaultUser = (u: Partial<User> = {}): User => {
 2  return {
 3    id: createId(),
 4    name: "",
 5    email: "",
 6    verified: false,
 7    admin: false,
 8    ...u,
 9  };
10};

Now when we want to create a new user object, we can do this:

1const user = defaultUser({
2  name: "user name",
3  email: "user@name.com",
4});

If we need to make a change to the user interface, all we have to do is change the defaultUser function and all objects that were created by the factory will most likely "just work." Obviously if we removed name, email, or changed the names of those properties, we will have to update them manually, but that is usually rare.

So what are the benefits of this paradigm?

Easy to create an entity #

Creating a User entity becomes much easier. All we have to do is import and call defaultUser() and it will create the entity for us. In the beginning it seems very tedious to build these factories because it's a hand-written function, but we only have to write it once and then we permenantly enjoy the ease-of-use forever.

A single entry-point for creating an entity #

This is huge. There is only one entry-point for creating the User entity inside of your codebase. Once this entity factory is written, everywhere will instinctively use it because it is so easy to use.

Easy to bake in sane defaults #

This was an idea that I got from working with golang. When creating a struct in golang, it's very common to create a function like NewUser which does the exact same thing as the the typescript version written above.

In golang, there are zero values which provide sane defaults for all primitive types

If it's a string the zero value is '' and if it's a number it's a 0. I have applied the same concept to typescript and it works incredibly well.

I bet you already do this an you don't even realize it. For example, if a property is a an array then the zero value is an empty array []. That way, when we need to perform a map or filter on the array we don't have to check if the value is an array. Make sense, right? Well I just apply the same concept for all types in typescript. Basically, I try to avoid null or undefined values as much as possible. I elaborate on this concept in a previous blog article: Death by a thousand existential checks

Ability to override defaults #

Because we accpe a partial User object as a parameter to our factory, it's easy to override the defaults. This makes it easy to use: pass in what you want to change and let the defaults do the rest of the entity building.

No libraries required #

The paradigm is so simple you don't really need a library to handle the factory building. I've thought about creating a library for this paradigm but honestly, part of the appeal is that it's just a simple function. It's easy to read, easy to understand, and there's no magic required. All we do is leverage typescript syntax and that's it.

Conclusion #

It might seem obvious, but entity factories are a very critical aspect to how I build maintainable code that is easy to update, extend, or modify entities over time.


I have no idea what I'm doing. Subscribe to my rss feed to read more posts.