Kealan Parr Software engineer, technical writer and member of the Unicode Consortium.

Writing a constructor in TypeScript

4 min read 1279

TypeScript Logo Over a Colorful Thrown-Paint Background

Any mature TypeScript codebase will typically make heavy use of interfaces.

They are, after all, the building blocks of adding static compile-time checks on your code, and they ensure you are sensibly using the collective/custom types you define within your code.

Interface syntax is simple, and interfaces offer a host of advantages when used in your code, such as:

  • Produce simple, easily understood error messages
  • Sometimes compile faster than type definitions
  • Used heavily by the TypeScript community, so they are a common best practice, (the TypeScript documentation utilizes them heavily also)
  • The TypeScript team endorses interfaces, too. Daniel Rosenwasser, TypeScript’s program manager, has endorsed interfaces over type

Let’s learn more about constructors and how we’ll use constructors in interfaces:

Constructors

Constructors are also a code feature heavily used in TypeScript codebases too.

The TypeScript docs have a great example of constructor usage:

class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return "Hello, " + this.greeting;
  }
}

let greeter = new Greeter("world");

Constructors essentially allow the creation of objects from classes.

Classes act like a blueprint for typing the objects you create with the necessary properties and methods.

Constructor dependency injection

Constructors often make use of a code technique called dependency injection — that is key to utilizing them to their fullest potential.

This is where the dependencies necessary for the object we’re going to create are passed into the constructor.

We made a custom demo for .
No really. Click here to check it out.

In the above example, we see we pass in the message argument into the constructor to allow unique customization of the object:

let computer = new Greeter("world");         // Hello world
let farewell = new Greeter("and goodbye!");  // Hello and goodbye!

The ability for the same class (the Greeter class) to provide different results from a method call is often called polymorphism.

Multiple constructors

A final important thing to remember when using constructors is that you cannot use multiple constructors implementations — like you can in other object-orientated languages.

An example of multiple constructors would be like so:

class Animal {

  constructor() {
  }

  constructor() {
  }
}

The above code won’t compile and logs the error Multiple constructor implementations are not allowed.

If you need to use multiple constructors to provide different functionality from a base class, there are ways of doing this, but you can only use one implementation.

If you need different constructors — there are ways to work around this though, you can type multiple constructors — you just can’t implement them.

A real-world example would look like:

class DemoClassTest {
  constructor(x : string, y:string);
  constructor(x : number);
  constructor(x : number, y:string, z:string);
  constructor(...myarray: any[]) {

    if (myarray.length === 2) {
      console.log('two argument constructor called here !!');
      return;
    }
    if (myarray.length === 3) {
      console.log('three argument constructor called here !!');
      return;
    }
    if (myarray.length === 1) {
        console.log('one argument constructor called here !!');
        return;
      }
  }
}
let a = new DemoClassTest('hello', 'bye');
let b = new DemoClassTest(1);
let c = new DemoClassTest(100, 'str1', 'str2');

Using a constructor on an interface

We’ve discussed the more common use cases for utilizing constructors, but their functionality doesn’t end there.

Sometimes, as part of a design pattern or for certain use cases, developers may want to specifically create an instance variable from an interface.

A simple example of an interface we might want to construct could be:

interface Animal {
  numLegs: number,
  wings: boolean
}

But how we add a constructor to this type is not clear.

Even more confusingly, in the compiled JavaScript, the interface won’t even exist. It only exists to check our types and then will be totally removed, thanks to a process called type erasure.

So, let’s start with a failing example and solve it iteratively:

interface InterfaceWithConsturctor {
  config: string;
  constructor(config: string): { config: string };
}

class ConfigClass implements InterfaceWithConsturctor {

  public config = '';

  constructor(config: string) {
    this.config = config;
  }
}

The error we are currently facing is:

- Class 'ConfigClass' incorrectly implements interface 'InterfaceWithConsturctor'.
- Types of property 'constructor' are incompatible.
- Type 'Function' is not assignable to type '(config: string) => { config: string; }'.
- Type 'Function' provides no match for the signature '(config: string): { config: string; }'.

Even though our two constructors match (in the interface versus in the class implementing the interface), it throws an error and won’t compile.

You can see in the two code examples that they are using the same type, and, by the looks of it, should compile just fine.

Adding a constructor to a TypeScript interface

The docs include an example covering this exact scenario.

Our earlier examples are failing because, according to the docs, “when a class implements an interface, only the instance side of the class is checked. Because the constructor sits in the static side, it is not included in this check.”

This reads weirdly, but it essentially means that the constructor isn’t an instance type method.

By instance type method, we’re referring to a “normal” function that would be called with obj.funcCall() existing on the object instance, as a result of using the new keyword. The constructor actually belongs to the static type.

In this case, the static type means the type it belongs to, without instantiating it, e.g., InterfaceWithConsturctor.

To fix this, we need to create two interfaces: one for the static type methods/properties and one for the instance type methods.

Our new working example, inspired by the engineering lead of TypeScript, looks like this.

interface ConfigInterface {
  config: string;
}
interface InterfaceWithConsturctor {
  new(n: string): { config: string };
}
class Config implements ConfigInterface {
  public config: string;
  constructor (config: string) {
    this.config = config;
  }
}
function setTheState(n: InterfaceWithConsturctor) {
  return new n('{ state: { clicked: true, purchasedItems: true } }');
}
console.log(setTheState(Config).config);

This now logs as { state: { clicked: true, purchasedItems: true } }.

Benefits to using TypeScript interface constructors

By using this language feature, you can create more composable objects that don’t rely on inheritance to share code.

With a constructor on the interface, you can specify that all of your types must have certain methods/properties (normal interface compliance) but also control how the types get constructed by typing the interface like you would with any other method/property.

We are relying on abstractions rather than concretions. There’s an example from the old TypeScript docs to highlight this.

The old docs are still valid TypeScript, and they’re a really clear example of what we’re discussing – so I have kept the legacy URL for clarity.

//
// Instance and static interfaces
//
interface ClockConstructor {
  new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
  tick(): void;
}

function createClock(
  ctor: ClockConstructor,
  hour: number,
  minute: number
): ClockInterface {
  return new ctor(hour, minute);
}

//
// Clock implementation classes
//
class DigitalClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("beep beep");
  }
}
class AnalogClock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {
    console.log("tick tock");
  }
}

// Now we allow relatively generic (but typed!) creation of Clock classes
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

Here, we are creating a strictly typed constructor function with the arguments we need other classes to use, but at the same time, allowing it to be generic enough it fits multiple use-cases.

It also ensures we are keeping low coupling, high cohesion in our code.

Conclusion

I hope this has explained not only how to add a constructor onto an interface, but some of the common use cases for when and why it might be done, as well as the syntax of how you can achieve it.

It is a common enough occurrence that the docs even explain the basic approach, and it is useful to understand the two sides of static versus instance scope in the underlying JavaScript/TypeScript world.

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Kealan Parr Software engineer, technical writer and member of the Unicode Consortium.

Leave a Reply