I have recently found myself needing a type for class constructors that is at once generic and tight enough to ensure a genuine constructor. This is useful in situations where you must handle a variety of classes - those that come from other libraries or applications that you cannot control.

When writing your own dependency injection container or some other generalised library you cannot know what class constructors might be passed in. The code just needs to know that calling the constructor will lead to an instance.

I’ve settled on a type that I use for this situation. Whilst the type itself will land up being rather small, and some might say simple, it is, nevertheless not particularly obvious.

An example class constructor we might want to pass to other functions could be something like this little Author class definition.

class Author {
  public readonly age: number = NaN;
  public readonly email: string = "";
  public readonly name: string = "";
}

When creating API endpoints it is common to accept a JSON string in the request body that needs to be validated and, ideally where TypeScript is involved, correctly typed. To facilitate this we might have a function that takes a JSON string and converts it into an instance of a predetermined class.

This code is for demonstration and not production ready, but you could imagine it handling requests for a REST API.

/**
 * Using a given JSON string construct and populate an instance of the
 * supplied class constructor
 * @param source JSON request payload string that the API receives
 * @param destinationConstructor a class constructor
 */
const json2Instance = (source: string, destinationConstructor: any) =>
  Object.assign(new destinationConstructor(), JSON.parse(source));

const simon = json2Instance('{"name":"simon"}', Author);

This looks like it will work nicely, but in practice by using the any type on the destinationConstructor the types have been broken. This prevents type checking from working correctly, which also means that auto hinting will no longer work in developer’s IDEs or editors. So, we need to come up with a type for it so that json2Instance() allows the type signatures to flow through.

Types given as any effectively block all benefits of using TypeScript in the first place - there is a place for them, but that is another article entirely.

Looking at the types available in lib.es5.d.ts from the TypeScript language source code shows us what a constructor type could look like. There are types for all the native JavaScript constructors such as Number, String, Function and Object.

Both the Function and Object constructor types have additional properties that are possibly undesirable, so instead of using them directly or extending them we’ll create our own.

The most basic of constructor types could be defined as a function that returns an Object, but this is not quite what we are after as you might see.

type Constructor = new () => Object;

const json2Instance = (source: string, destinationConstructor: Constructor) =>
  Object.assign(new destinationConstructor(), JSON.parse(source));

Unfortunately, we’re still losing the type - we know it’s an Author, but this constructor type is telling TypeScript that it is a standard or plain old JavaScript Object. To tighten this up it is necessary to introduce generic types.

Before we move onto that though - a quick word on constructors that take arguments (args in the example code). To handle constructor functions that take arguments we can make use of the spread operator in the constructor type.

class AuthorWithConstructor extends Author {
  public readonly greeting!: string;
  constructor(name: string = "") {
    this.greeting = `Top of the muffin to you, ${name}`;
  }
}
type Constructor = new (...args: any[]) => Object;

This Constructor type is still indicating that the returned value is of type Object, which as we discovered before is breaking the typings for the json2Instance() function. Using TypeScript’s generics features it is possible to correct this behaviour.

type Constructor<T> = new (...args: any[]) => T;

By passing in the type (T) as a generic type argument it is possible to state that the return value of the constructor is of type T. To use this new type there are a couple of changes to make to the json2Instance() function to allow for the generic to be specified.

const json2Instance = <T>(
  source: string,
  destinationConstructor: Constructor<T>,
): T => Object.assign(new destinationConstructor(), JSON.parse(source));

When called the type (Author) now flows through as the generic T type.

const simon = json2Instance('{"name":"simon"}', Author);
console.log({ age: simon.age, nextYear: simon.age + 1 });
// no type errors because it knows age is number in the addition

// also in your IDE/editor you'll now get code completion/suggestions where you type
// the instance name `simon` and get a list of possible properties:
// simon.
//   |--> age
//   |--> email
//   |--> name

So, we have solved the problem where the type of the constructor (Author) is known. However, it is not always possible or desirable to know the type. Think of defining other types or interfaces that have a constructor as a property.

A limited example of this in action might be to have a list of class constructors.

type ControllerList = Constructor[];

We do not know the class type of the constructors in the list and it is not necessary to know for our calling code. It just needs to know it can create an instance. By providing a default for the type argument (T) of {} we allow implementing types avoid providing a type argument that they cannot know.

type Constructor<T = {}> = new (...args: any[]) => T;

By default the type will be a constructor that returns an object, but as before if you specify the type argument T then it will use the given type.

It is possible to tighten up our definition a little further using the extends keyword so that any T must have an object type - as all constructors do.

type Constructor<T extends {} = {}> = new (...args: any[]) => T;

And, there you have it. A constructor type that is at once flexible and restrictive.