DEV Community

Cover image for Writing a constructor in TypeScript
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Writing a constructor in TypeScript

Written by Kealan Parr✏️

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

Constructing interfaces

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
}
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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; }'.
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

alt text

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.

Top comments (1)

Collapse
 
mosesakor profile image
mosesakor

hmm