Named Constructors in TypeScript: Constructing Classes/Objects dart style
I was looking for a cleaner and reusable way to construct classes in TypeScript - like it is achieved using named constructors in Dart. I found a way to do this using a bit of help from ChatGPT, generics and the class-transformer
library and thought I'd share it here. ChatGPT even co-authors this post with me, as I was too lazy to sum it up myself.
const john = new Human();
john.name = 'John';
john.age = '27';
const john = new Human({ name: 'John', age: '27' });
I will walk you through creating a reusable generic base class that allows for object construction in TypeScript, much like you might see in Dart.
Setting the Stage
Let's start with a simple Human
class which extends from a BaseClass
:
class Human extends BaseClass {
name: string;
age: string;
}
Our goal is to create a new instance of Human
like this:
const john = new Human({ name: 'John', age: '27' });
We aim to pass an object during class instantiation, hence creating a constructor with named parameters.
Leveraging class-transformer
One of the libraries that can make this task easier is class-transformer
. It offers a neat function, plainToInstance
, which can convert plain JavaScript objects to class instances.
Crafting the BaseClass
Let's see how we can construct a BaseClass
to achieve this:
import { plainToInstance } from 'class-transformer';
class BaseClass<T> {
constructor(classObject: Partial<T> = {}) {
Object.assign(this, plainToInstance(this.constructor as any, classObject));
}
}
This base class uses TypeScript's generic types, where T
is the type of the object passed to the constructor. It uses plainToInstance
to convert this object into an instance of the class, then merges this instance with this
using Object.assign
.
Here's the catch: We're using this.constructor as any
to bypass TypeScript's type system. The constructor will indeed be a function at runtime, so this is a safe cast.
It would be cool if we didn't have to make the class generic, and instead infer the type from the constructor. Unfortunately AFAIK, TypeScript doesn't allow us to do this. If you have a solution, please let me know in the comments!
Bringing it Together
Now we can define our Human
class:
class Human extends BaseClass<Human> {
name: string;
age: string;
}
With this setup, TypeScript will check that the object you're passing to the constructor matches Partial<Human>
, giving you compile-time checking for the properties. This means if you try to pass an object with properties not declared in Human
, TypeScript will give you a type error:
// This is valid
const john = new Human({ name: 'John', age: '27' });
console.log(john);
// This will give a type error
// const invalidJohn = new Human({ someNonExistingProperty: '123', age: '27' });
// console.log(invalidJohn);
Wrapping Up
This technique offers a great way to create reusable and extensible base classes in TypeScript. It makes your code type-safe and allows you to leverage TypeScript's static type checking while beautifully creating new class instances.
Remember, this technique still depends on class-transformer
, and it behaves according to any decorators you've placed on your class properties. If you have complex transformations, you will still need to annotate your properties with the appropriate decorators. But for plain properties like string
, number
, boolean
, and so on, no decorators are needed.
Feel free to experiment with this pattern and see how it can make your TypeScript code cleaner and more understandable!
Using a static method instead of generic class
TypeScript does not provide a built-in way to infer the type of this
within the class constructor, which is necessary to remove the generic from the base class.
This is a complex topic within TypeScript because of the way classes and the this
keyword works. When you are in the constructor, the class instance hasn't been fully constructed yet, hence this
is not fully typed. The type of this
only becomes clear after the constructor has run and the instance has been constructed.
One way around this is to move the transformation logic to a static factory method. Here's how:
import { plainToInstance } from 'class-transformer';
class BaseClass {
static create<T>(this: new () => T, plainObject: Partial<T>) {
return plainToInstance(this, plainObject) as T;
}
}
class Human extends BaseClass {
name: string;
age: string;
}
const john = new Human({ name: 'John', age: '27' }); // The old way, using 'new Human'
const john = Human.create({ name: 'John', age: '27' });
console.log(john);
In this code, the create
method infers the type T
from the this
value, which is the class constructor when you call Human.create(...)
. It returns an instance of the class, created and initialized from the plain object.
Note: that this is not exactly the same as having the transformation logic in the constructor, because you're now calling a static method instead of using the new
keyword to create the instance. But it does allow you to create typed instances from plain objects without making BaseClass
generic. This solution also does not prevent you from creating a new instance of the class with the new
keyword, you just need to make sure to also provide a standard constructor if you wish to allow this.