The Javascript ecosystem has many libraries available. In fact, npm has over 350,000 packages and counting, which is a considerable quantity of code. The problem comes mainly because most of these have been done in plain Javascript, and they haven't adopted the type safety of TypeScript. At Stack Builders we embrace type safety since it makes the code more expressive and maintainable in the long-term. It's because of this that today we present to you our type contribution to the factory-girl library, with a previous explanation about why is it important and how we tackled the problem of adding types to it.
The Factory Method pattern
When writing automated tests every developer will inevitably need to generate fake data. The issue at hand comes when we need to test a class or a Javascript object, and we depend on a concrete implementation of it. This makes the test case and the class tightly coupled, because said class can change its constructor over time causing the tests to break. To solve this we can rely on the factory method pattern, which is done by calling a factory method instead of the specific class constructors to generate concrete objects.
There are many libraries for the Javascript ecosystem that help us implement this pattern, but in this article, we'll focus on one in particular.
Hello, factory-girl!
factory-girl is a popular Javascript factory library, inspired by Ruby's factory-bot (it was formerly known as factory-girl in the ruby ecosystem as well, but they changed the name in 2017 for a few reasons described here). This library allows us to generate factory methods to be used in our tests. Consider the following example:
const factory = require("factory-girl").factory;
const ToughGuy = require("./models/toughguy");
factory.define("tough-guy", ToughGuy, {
firstName: "John",
lastName: "Rambo",
email: factory.seq("ToughGuy.email", (n) => `guy${n}@fbi.com`),
});
Here we defined a factory method for a ToughGuy model class, which allows us to repeatedly create it for our tests. If we use it as is, it will always create it setting firstName
to "John" and lastName
to "Rambo". The email
field uses the factory.seq
method, so when we generate the first object with this factory it will set the field to guy1@fbi.com
. The second object will be generated with guy2@fbi.com
, and so on. This factory will be used as follows:
factory.build("tough-guy").then((guy) => {
// Factory returns a promisified result.
// In this case "guy" will be:
// {
// firstName: "John",
// lastName: "Rambo",
// email: "guy1@fbi.com"
// }
});
factory.build("tough-guy", { lastName: "Wick" }).then((guy) => {
// The data for the factory can be overwritten.
// In this case "guy" will be
// {
// firstName: "John",
// lastName: "Wick",
// email: "guy2@fbi.com"
// }
});
With this approach we can generate as many ToughGuy
instances as we need without having to create them using the class constructor. This brings a second problem to the table, which is...
Where are my types?
The dynamic nature of Javascript allows us to create these objects easily without worrying too much about the individual types for the class attributes. Because of this we could potentially do the following:
factory.build("tough-guy", { lastName: true, quote: "Hahaha!" }).then((guy) => {
// Here "guy" will be:
// {
// firstName: "John",
// lastName: true,
// email: "guy1@fbi.com"
// }
});
As you can see, factory-girl doesn't care at all about the primitive types that the factory method generates. Moreover, it doesn't care that we sent a quote
attribute that isn't on the ||ToughGuy
definition. This could potentially cause us to write tests that don't check an object thoroughly and lead us to test cases that shouldn't even exist. To solve this we need to bring another friend to the discussion.
TypeScript to the rescue
TypeScript helps us bring some of the benefits of the static analysis to the table, so we could define better semantics and catch some trivial errors like typos or extra fields. Let's rewrite our example using it:
// The ToughGuy class doesn't have types,
// so we would use an interface to force
// every ToughGuy object to adhere to
// specific types
//
// interface ToughGuy {
// firstName: string;
// lastName: string;
// email: string;
// }
factory.build("tough-guy", { lastName: true }).then((guy) => {
// Here "guy" will be:
// {
// firstName: "John",
// lastName: true,
// email: "guy1@fbi.com"
// }
});
So this gives us the same output from last time. It didn't check types at all, even though we wrote it using TypeScript. Why isn't this adhering to the ToughGuy
interface that we defined earlier? The answer is simple: The factory-girl library doesn't have type definitions.
Adding type definitions to factory-girl
DefinitelyTyped contains external type definitions for libraries that are written in plain JS. Sadly there weren't type definitions for factory-girl, which made the factory methods a bit unsafe for testing. In Stack Builders we recently added the types for this library. Let's take a look at the definition for factory.define
and factory.build
:
define<T>(
name: string,
model: any,
attrs: T,
options?: Options<T>
): void;
build<T>(
name: string,
attrs?: Partial<T>
): Promise<T>;
So what does this mean? In short, define
is tied to a generic type T
which forces us to create an object with the attributes in it. Going back to our example we can be completely sure that when defining a factory for a ToughGuy
we only use the attributes defined the ToughGuy
interface.
So what about the build
function? Let's dig in a bit deeper:
build<T>
tells us that when using this function we need to send a type to it, which will be tied to the function's arguments and return valuesname: string
just gives a type to the factory name. Nothing too complicated hereattrs?: Partial<T>
is the important part. Here we're telling the compiler to use the genericT
type defined at the start. So in this case we won't be able to send additional attributes that are not in the original type or an object with the wrong types.- After building the object we get a promisified result of the original
T
type, which is defined by: Promise<T>
When using these type definitions we need to be strict with the things we send to our build method, since otherwise the TypeScript compiler will complain. This brings additional safety to the factory functions and lets us write more consistent tests.
But wait... What does that Partial<T> mean?
Partial types were introduced in TypeScript 2.1. Under the hood a partial is an alias that looks similar to this:
type Partial<T> = {
[P in keyof T]?: T[P];
};
This Partial type flags all properties in T
as optional, but you are still allowed to use all of the type's properties if needed. Let's check its behavior in our previous example:
// Let's assume for a moment that the type
// definition for the "build" function is:
build<T>(name: string, attrs?: T): Promise<T>;
interface ToughGuy {
firstName: string;
lastName: string;
email: string;
}
factory.build<ToughGuy>(
"tough-guy",
{ lastName: true }
).then(guy => {
// The code will not compile, since the object
// we're sending in the attributes parameter
// doesn't have a `firstName` or an `email`
// in its properties. It will also fail because
// `lastName` should be a string, not a boolean
})
The function definition expects an object that's fully compliant with T
, but in this case we only need to send a subset of the type's properties. In versions earlier to 2.1 we could do something like this:
interface ToughGuy {
firstName: string;
lastName: string;
email: string;
}
// Define another interface with all optional
// attributes
interface ToughGuyPartial {
firstName?: string;
lastName?: string;
email?: string;
}
// Then our type definition would be something
// like this
build<T, U>(
name: string,
attrs?: U
): Promise<T>;
// Which would be used like this
factory.build<ToughGuy, ToughGuyPartial>(
"tough-guy",
{ lastName: "McClane" }
).then(guy => {
// ...
});
So partials let us reuse a type and have only a subset of that type's properties. Our original example would be:
// Our type definition is
build<T>(
name: string,
attrs?: Partial<T>
): Promise<T>;
// Usage would be
factory.build<ToughGuy>(
"tough-guy",
{ lastName: "McClane" }
).then(guy => {
// Compiles correctly!
});
In conclusion, adding TypeScript types to an existing library is a great way to make it more robust and safe. You can see a usage example for this library and its types in this repository. We also encourage you to check your favorite libraries for types in the DefinitelyTyped repository, and to add your own types if there are none. This will help you and all of the library's users to have an additional safety check so the bugs can be minimized.