This example of a simple factory pattern in TypeScript can be useful if you want to create objects of a specific type based on some given inputs, but you don’t want the client code to have to know about the specific implementation details of each object type.
In this example, the VehicleFactory
class acts as a central point in the code – the factory – for creating Vehicle
objects.
interface Vehicle {
move(): string;
}
class Car implements Vehicle {
move() {
return "Driving a car...";
}
}
class Bike implements Vehicle {
move() {
return "Riding a bike...";
}
}
export class VehicleFactory {
static createVehicle(type: string): Vehicle {
switch (type) {
case "car":
return new Car();
case "bike":
return new Bike();
default:
throw new Error("Invalid vehicle type");
}
}
}
const car = VehicleFactory.createVehicle("car");
car.move(); // Outputs: 'Driving a car...'
const bike = VehicleFactory.createVehicle("bike");
bike.move(); // Outputs: 'Riding a bike...'
Code language: TypeScript (typescript)
The createVehicle
method takes a string parameter that specifies the type of vehicle to create, and it returns an object of the corresponding type.
These createVehicle
methods is static
so we do not need to create a new instance of the VehicleFactory
before we can call / use this method.
The client code can then call the createVehicle
method of the factory and pass in the desired vehicle type, and the factory will handle the rest.
This works as a basic starting point of our factory pattern example in TypeScript, so lets add some unit tests and then see if we can improve the code.
Example Unit Tests for Our TypeScript Simple Factory Pattern
Life isn’t worth living if you haven’t added some unit tests.
Here are some example unit tests for the above simple factory pattern implementations:
import { VehicleFactory } from "./factory";
describe("VehicleFactory", () => {
it("should create a car", () => {
const vehicle = VehicleFactory.createVehicle("car");
expect(vehicle.move).toBeDefined();
expect(vehicle.move()).toEqual("Driving a car...");
});
it("should create a bike", () => {
const vehicle = VehicleFactory.createVehicle("bike");
expect(vehicle.move).toBeDefined();
expect(vehicle.move()).toEqual("Riding a bike...");
});
it("should throw an error for an invalid vehicle type", () => {
expect(() => VehicleFactory.createVehicle("boat")).toThrowError(
"Invalid vehicle type"
);
});
});
Code language: TypeScript (typescript)
These tests cover three test cases.
The first two tests behave almost identically and could almost certainly be refactored to be more streamlined if further VehicleType
‘s were introduced in to the system.
Some people disagree with testing .toBeDefined
, as if the method were not defined then the test on lines 9 and 16 would fail anyway. Personally I see no harm in adding this.
The more interesting test is when expecting an error. This one tricked me for a while when learning to use Jest. Note that the expect
code that tests createVehicle
is itself wrapped in a function. That function is then checked by Jest to assert against the error message.
With some passing tests we can now improve the TypeScript code with more confidence.
Example Refactoring #1
A possible improvement here is to use an enum
for the allowable types of vehicle that exist in our system. In my opinion, enum
‘s or other similar constructs (an object with set keys, symbols, etc) make the code easier to read and less likely to contain errors.
export enum VehicleType {
Car = "car",
Bike = "bike",
}
Code language: TypeScript (typescript)
We could then use the new enum
values in our tests:
import { VehicleFactory, VehicleType } from "./factory";
describe("VehicleFactory", () => {
it("should create a car", () => {
const vehicle = VehicleFactory.createVehicle(VehicleType.Car);
expect(vehicle.move).toBeDefined();
expect(vehicle.move()).toEqual("Driving a car...");
});
it("should create a bike", () => {
const vehicle = VehicleFactory.createVehicle(VehicleType.Bike);
expect(vehicle.move).toBeDefined();
expect(vehicle.move()).toEqual("Riding a bike...");
});
Code language: TypeScript (typescript)
We export
ed the enum
from the implementation code, so we can import
it (line 1) in our tests.
Then we replace the literal strings of car
and bike
with the two enum
values we have restricted down too.
At this points the tests should still pass.
Example Refactoring #2
Introducing the enum
was a good start.
However a consumer of our VehicleFactory.createVehicle
method can still pass in any string
value they like:
static createVehicle(type: string): Vehicle {
Code language: JavaScript (javascript)
Given we have the more specific VehicleType
available, we should use this in place of the string
type in this method signature:
static createVehicle(type: VehicleType): Vehicle {
Code language: JavaScript (javascript)
However, this ‘breaks’ things:
So, what’s the issue here?
Well, we have told TypeScript that our VehicleFactory.createVehicle
method expects only values from our VehicleType
enum options. And then we have passed in some literal string values.
From the screenshot above you can probably understand why boat
is underlined in red in the test file.
But why are the strings of car
and bike
seen as TypeScript errors when they match the string values in the enum?
TS2345: Argument of type '"car"' is not assignable to parameter of type 'VehicleType'.
Code language: JavaScript (javascript)
To the very best of my understanding (aka, I may be wrong), the issue here is that the string enum is a more specific type of the string itself.
So VehicleType.Car
(the string enum value of “car”) is a subtype of the string literal “car”.
TypeScript can compare VehicleType.Car
to "car"
but it cannot compare "car"
to VehicleType.Car
.
If that doesn’t make a great deal of sense then maybe this explains it better. If not, I would say do not worry too much as this is a detail that is interesting to know, but will not directly impact you in 99% of your day to day usage of TypeScript. At least, it never has for me.
Exhaustive Switch
Fixing that failing test is actually really challenging.
Think about it for a moment.
Our test says that when we give an unexpected parameter to our VehicleFactory.createVehicle
method, our code should throw an error.
Our implementation is set up that way, and we saw before that the tests do pass.
But now we have provided a more restrictive function signature:
static createVehicle(type: VehicleType): Vehicle {
Code language: JavaScript (javascript)
We control the enum
and we therefore know we only ever have two possible values. Car, or Bike.
TypeScript knows this, too.
So do we even need the default
case in our switch
?
static createVehicle(type: VehicleType): Vehicle {
switch (type) {
case "car":
return new Car();
case "bike":
return new Bike();
default:
throw new Error("Invalid vehicle type");
}
}
Code language: TypeScript (typescript)
The answer here is … yes 😧 then no 🤗
Let’s remove the third unit test case entirely, and the default
clause:
export class VehicleFactory {
static createVehicle(type: VehicleType): Vehicle {
switch (type) {
case "car":
return new Car();
case "bike":
return new Bike();
}
}
}
Code language: JavaScript (javascript)
This makes TypeScript sad:
TS2366: Function lacks ending return statement and return type does not include 'undefined'.
Code language: JavaScript (javascript)
And the tests still fail:
The issue here is subtle.
static createVehicle(type: VehicleType): Vehicle {
switch (type) {
case "car":
return new Car();
case "bike":
return new Bike();
}
}
Code language: TypeScript (typescript)
We saw that TypeScript can convert the enum value to the matching string, so technically this code does work.
The problem is that we told TypeScript to expect our method to return a Vehicle
, and because we have only checked two literal string cases, what happens if anything else is passed in? Something new added to the VehicleType
perhaps?
Well, that would not match the two specified case
statements, and we haven’t defined what to do in that case. So our system would blow up.
As I said, the fix is subtle, but also really simple:
static createVehicle(type: VehicleType): Vehicle {
switch (type) {
case VehicleType.Car:
return new Car();
case VehicleType.Bike:
return new Bike();
}
}
Code language: TypeScript (typescript)
We switched to use the enum
values in our case
clauses, and therefore we no longer need the default
.
This is called an exhaustive switch statement. We have covered all the possible permutations.
Our tests therefore are now as simple as:
import { VehicleFactory, VehicleType } from "./factory";
describe("VehicleFactory", () => {
it("should create a car", () => {
const vehicle = VehicleFactory.createVehicle(VehicleType.Car);
expect(vehicle.move).toBeDefined();
expect(vehicle.move()).toEqual("Driving a car...");
});
it("should create a bike", () => {
const vehicle = VehicleFactory.createVehicle(VehicleType.Bike);
expect(vehicle.move).toBeDefined();
expect(vehicle.move()).toEqual("Riding a bike...");
});
});
Code language: PHP (php)
And the last question then, what happens if we add a new value to the VehicleType
enum?
We added VehicleType.Bus
and immediately our tests break and the issue reoccurs.
Real World Example Of TypeScript Simply Factory Pattern
How does this example of the TypeScript simple factory pattern apply beyond the examples above?
I figured I would add this in as it’s rare for me to come across class
based TypeScript code out there in the real world. It is probably more prevalent in certain frameworks (Angular?) than it is in React projects that I tend to work on.
Here’s two variants you may see that both satisfy the test cases:
interface Vehicle {
move(): string;
}
const car: Vehicle = {
move: () => "Driving a car...",
};
const bike: Vehicle = {
move: () => "Riding a bike...",
};
export class VehicleFactory {
static createVehicle(type: VehicleType): Vehicle {
switch (type) {
case VehicleType.Car:
return car;
case VehicleType.Bike:
return bike;
}
}
}
Code language: TypeScript (typescript)
And / or:
type Vehicle = {
move(): string;
};
const car: Vehicle = {
move: () => "Driving a car...",
};
const bike: Vehicle = {
move: () => "Riding a bike...",
};
export class VehicleFactory {
static createVehicle(type: VehicleType): Vehicle {
switch (type) {
case VehicleType.Car:
return car;
case VehicleType.Bike:
return bike;
}
}
}
Code language: TypeScript (typescript)
The differences are subtle – type
versus interface
– which is something of an interesting debate in itself.
Note that this code is only comparable because the implementations are simple.
The nice part about factories are they can hide away a lot of complexity – if you were simply creating one method objects like the above, a factory may very possibly be overkill.