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 isVerified: boolean;
6 isAdmin: 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 isVerified: false,
6 isAdmin: 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 createUser = (u: Partial<User> = {}): User => {
2 return {
3 id: createId(),
4 name: "",
5 email: "",
6 isVerified: false,
7 isAdmin: false,
8 ...u,
9 };
10};
Now when we want to create a new user object, we can do this:
1const user = createUser({
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 createUser
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 createUser()
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 permanently 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 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 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 accept 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.
Use it for mock data #
Historically this is where I've seen it used in projects. For example, in ruby
fabrication
. Entity factories make it easy to
generate entities that conform to a specific schema and sane defaults.
1function updateUserEmail(u: User, email: string): User {
2 // ...
3}
4
5it("should update email", () => {
6 const user = createUser({ email: "harry.potter@hogwarts.com" });
7 const nextEmail = "potter.harry@hogwarts.com";
8 const actualUser = updateUserEmail(user, nextEmail);
9 expect(actualUser.email).toEqual(nextEmail);
10});
Use it for your ETL
pipeline #
I also like to use it as the T
in my ETL
pipeline when synchronizing data in
the FE. Below is some scaffolding for how I would model a typical ETL
pipeline
for my FE apps:
1interface Ok<E> {
2 ok: true;
3 value: E;
4}
5interface Err {
6 ok: false;
7 err: Error;
8}
9type Maybe<E> = Ok<E> | Err;
10
11const createOk<E>(value: E): Ok<E> => ({ ok: true, value });
12const createErr(err: Error): Err => ({ ok: false, err });
13const isOk<E>(m: Maybe<E>): Ok<E> => m.ok;
14const getValue<E>(m: Ok<E>) => m.value;
15
16interface UserResponse {
17 id: string;
18 name: string;
19 email: string;
20 is_verified: boolean;
21 is_admin: boolean;
22}
23
24// convert a `UserResponse` to a `User` while also performing schema validation
25function deserializeUser(u: UserResponse): Maybe<User> {
26 const user = createUser({
27 id: u.id,
28 name: u.name,
29 email: u.email,
30 isVerified: u.is_verified,
31 isAdmin: u.is_admin,
32 });
33 // schema validation
34 if (!user.email) {
35 return createErr(new Error("email empty"))
36 }
37 return createOk(user);
38}
39
40async function fetchUsers() {
41 // extract from api
42 const resp = await fetch("/users");
43 if (!resp.ok) {
44 return;
45 }
46 const data = await resp.json();
47 // transform
48 const users = data.users.map(deserializeUser).filter(isOk).map(getValue);
49 // load into state
50 localStorage.setItem("users", users);
51}
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.