Factory.ts: A Factory Generator for Test Data Using TypeScript

I’ve been using TypeScript on a React/Redux project, and I’m really enjoying it.

A year and a half ago, I tried to use TypeScript with an Angular project, and I found that it didn’t add that much. But with version 2.0 and on, TypeScript has really come into its own. Structural typing allows you to express concepts in TypeScript that I’ve never been able to express before. In particular, mapped types are just insanely useful.

Mapped Types Explained

A mapped type is a type which essentially derives from another type. Say, for example, I have a basic data structure:


interface Person {
  id: number
  firstName: string
  lastName: string
  age: number
}

Mapped types allow me to create a new type which has the same set of keys/properties as the original type, but with the values modified in some way. For example, I can just say Nullable<Person>, and that specifies the following:


type Nullable = {
  id: number | null
  firstName: string | null
  lastName: string | null
  age: number | null
}

Now every property will allow null as a value, whereas it won’t with just Person. Similarly, Partial<Person> will make a version where all the keys are optional:


type Partial = {
  id?: number
  firstName?: string
  lastName?: string
  age?: number
}

So I can make a Partial<Person> out of { age: 5 }, but not a Person.

Here’s what the definition of Nullable looks like:


type Nullable = {
  [P in keyof T]: T[P] | null
}

Basically, this says for every key P in type T, the value should have the type of the value at key P (that is, T[P]) or null.

Factory.ts

One place I’ve put this capability to good use is in generating factory data for my tests. I have a set of types that define my domain, including the types in my Redux state. Using mapped types, making factories for test data is a cinch. Moreover, I can use mapped types to provide guarantees you simply can’t get in a dynamic language like Ruby or JavaScript.

We’ll get into how this works, but first let’s look at how to build a factory for the Person type:


const personFactory = Factory.makeFactory({
  id: Factory.each(i => i),
  firstName: 'Bob',
  lastName: 'Smith',
  age: Factory.each(i => 20 + (i % 10))
});
const fred = personFactory.build({ firstName: 'Fred' });
// { id: 1, firstName: 'Fred', lastName: 'Smith', age: 21 }
const john = personFactory.build({ firstName: 'John', lastName: 'Johnson' });
// { id: 2, firstName: 'John', lastName: 'Johnson', age: 22 }

When calling makeFactory, if you fail to specify how each key/property in the type should be generated for the type in question, you will get a compile error. You will also get a compile error if you specify extra keys.

To specify the value for a key, you can either provide a default value or a function (via Factory.each) which takes a sequence number and returns a value of the correct type. Factory.each returns a Generator<U>, and makeFactory takes a Builder<T> as its argument.

Here’s the definition of Builder<T>:


type Builder = {
  [P in keyof T]: T[P] | Generator;
}

When defining a Builder<T>, each key must either be a raw value of the correct type for that key, or a Generator of same. When you call Factory<T>.build to create a T, I detect if the Builder<T> has a Generator for a given property or a raw value. If it is a generator, I call the generator function with a sequence number.

Because Generator<T> is defined as a class, I can check the constructor property to determine if a key in the Builder is for a Generator or is a raw value.


const v = (builder as any)[key];
let value = v;
if (!!v && typeof v === 'object') {
  if (v.constructor === Generator) {
    value = v.build(seqNum);
  }
}

You’ll notice that I do cast the Builder<T> to any so I can perform operations on it that the type system can’t verify. This is one of the subtle and awesome things about TypeScript–if you need to fall back to dynamism inside a particular function, you can still give the function a typed signature and thereby provide a really nice API to the consumer of the function.

I’ve dubbed this factory library factory.ts. I added a few more bells and whistles to the library that are worth mentioning:

Factory Extensions

Factory<T> has a method extend() which takes a Partial<Builder<T>> and returns a new Factory<T> with changes based on the partial builder you supplied. This allows you to derive from a base factory, so for instance, you might have adultFactory and childFactory as extensions of personFactory, with sensible defaults:


const adultFactory = personFactory.extend({ age: 35 });
const childFactory = personFactory.extend({ age: 7 });

Derived Values

Factory<T> also has methods to define some key/property of a type as being derived from other keys. Say, for example, we wanted to add a property fullName: string to Person, and make a factory such that by default the firstName and lastName specify the fullName. Here’s how we could create such a factory:


const personFactory = Factory.makeFactory({
  id: Factory.each(i => i),
  firstName: 'Bob',
  lastName: 'Smith',
  fullName: '',
}).withDerivation2(['firstName', 'lastName'], (firstNm, lastNm) => `${firstNm} ${lastNm}`);
const fred = personFactory.build({ firstName: 'Fred' });
// { ..., fullName: 'Fred Smith' };
const john = personFactory.build({ firstName: 'John', lastName: 'Johnson' });
// { ..., fullName: 'John Johnson' };

It’s worth noting that the values in the array of dependencies given to withDerivation2 must be keys in Person. If they are not, you will get a compile error. Additionally, firstNm and lastNm are inferred to be the type of Person.firstName and Person.lastName respectively. Finally, the lambda must return a value of the type of Person.fullName. Mapped types give us the ability to express very powerful concepts in a safe way.

Broader Application

I’m particularly enamored with TypeScript’s ability to let me specify which keys are dependent and then automatically provide those values–with the correct types–to the lambda.

My sincere hope is that this sort of capability will make libraries like Ember more amenable to use with TypeScript. I still think Ember is well-architected, but it can be difficult to scale an Ember app or to pick it up after some time. Part of the reason for this is the lack of types, which makes it hard to know what types of data are being used in your components, models, etc.

I’d also like to see mapped types used to facilitate property-based testing in TypeScript. Creating generators and shrinkers for structural types should be a breeze.

Closing Request

Checkout factory.ts for generating data in your TypeScript project. If there’s a feature you’d like to see added, send me a pull request. Happy Type-ing!

Conversation
    • Will Pleasant-Ryan Will Pleasant-Ryan says:

      Hi Candie,

      This post isn’t really about Angular factories versus Angular services. I do reference Angular early in the post, but only to mention how I did not find an older version of Typescript very helpful when writing an Angular app. The factory library this post discusses is mainly for generating data that matches a desired structure (e.g. for tests), and is independent of Angular. The word “factory” is meant to evoke the same meaning as in Ruby’s factory_girl.

  • Rudy Jahchan says:

    We love using `factory.ts` at Carbon Five. So much that I submitted a pull request. Would you kindly check it out? ;-)

    • Will Pleasant-Ryan Will Pleasant-Ryan says:

      Awesome, thanks for the PR! I have merged in your change and released factory.ts version 0.2.2 to npm.

  • Jesse Pinho says:

    Love this library! Thanks for open-sourcing it :) Just started playing with it today.

  • Comments are closed.