Use Static Factory Methods Instead of Constructors in TypeScript
The traditional way for a class in TypeScript to allow a user of the class to obtain an instance of it is to provide a public constructor. There is another useful technique that should be a part of every programmer’s toolkit.
A class can provide a public static factory method. Now don’t confuse this with the stuffy Factory Method pattern from that Design Patterns books. It’s not the same at all and has no direct equivalent in that book or other design pattern books. Also, using a static factory method instead of a public constructor has both advantages and disadvantages that you need to consider.
A static factory method is simply a static method on a class (or object) that returns an instance of the class (or object). Sounds too easy, right? You may be thinking, why would I ever do such a thing? That’s was constructors are for, right?
The first advantage of static factory methods is that, unlike constructors, they are not required to create a new object each time they are called. This allows immutable classes to use objects that are preconstructed, or to cache an instance as it is constructed, and then return is repeatedly to avoid creating unnecessary duplicate objects. It can greatly improve performance if the same or equivalent class instances are requested often (e.g. in a loop). This is even more true if they are expensive to create. When a static factory method returns the same object from repeated invocations it allows the class to maintain strict control over what, and how many, instances exist at any one time. A class that does this is said to be instance controlled.
The second advantage of static factory methods is that, unlike constructors, they have descriptive self-documenting names. If the parameters to a constructor do not, in and of themselves, describe the object being returned, a static factory with a well-chosen name is easier to use and the resulting client code easier to read. Typically, programmers allow a object to be instantiated by varying the types and number of arguments. Often, it can be confusing to users exactly what should be passed to the constructor for all of the possible ways the author provided to construct the object. Users will not know what exactly to provide without referring to the class documentation. However, by providing several static factory methods with different names and arguments, the class author’s intent becomes crystal clear.
The main limitation of providing only static factory methods is that classes without public or protected constructors cannot have child classes in TypeScript. After all, child classes absolutely must call super() and if the parent class has a private constructor, then it cannot be sub-classed. Most people (me included) would argue this is a disguised blessing because it encourages composition over inheritance (or prototype chaining).
Another difficulty of static factory methods is that they are hard to find because they do not stand out as obviously as constructors do. You can reduce this issue by either drawing attention to static factories in documentation and/or by adhering to common naming conventions.
Here are some common names and code examples I have seen for static factory methods. There are others I am sure.
Option #1: “from” Prefix Naming
This type of static factory method is a type-conversion that takes a single parameter and returns a corresponding instance of this class.
const bigInteger = MyBigInteger.fromString('999999999999999');
Option #2: instance/getInstance/create/newInstance/etc Naming
This type of static factory method takes one or more parameters and returns an instance of the class. My personal favorite is “create”, but “instance” is just as common.
const myFoo = Foo.create('a', 2);
Option #3: “with” Prefix Naming
This type of static factory method is used to provide different instances based on different sets of parameters passed to the method. The full name of the static factory method should be descriptive.
const customPizza = Pizza.withToppings(['sausage', 'pepperoni', 'mushrooms']); const hawaiianPizza = Pizza.withCommonNameStyle('hawaiian');
To summarize, both static factory methods and public constructors have their uses. Having both in your toolkit is important. In many case static factories are preferable and it’s a good idea to consider both instead of providing public constructors by default.
UPDATE: A helpful Reddit reader pointed out that unlike constructors, static factory methods can also be async. This is awesome of course! Thanks /u/modec!