Article summary
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!
I disagree. Look at https://blog.thoughtram.io/angular/2015/07/07/service-vs-factory-once-and-for-all.html
Candie
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.
We love using `factory.ts` at Carbon Five. So much that I submitted a pull request. Would you kindly check it out? ;-)
Awesome, thanks for the PR! I have merged in your change and released factory.ts version 0.2.2 to npm.
Love this library! Thanks for open-sourcing it :) Just started playing with it today.