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:
type
definitionstype
Let’s learn more about constructors and how we’ll use constructors in interfaces:
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.
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.
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.
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');
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.
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 } }
.
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.
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.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSimplify component interaction and dynamic theming in Vue 3 with defineExpose and for better control and flexibility.
Explore how to integrate TypeScript into a Node.js and Express application, leveraging ts-node, nodemon, and TypeScript path aliases.
es-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.