Let's start with the simple interface in your example:
interface ClockInterface {
tick();
}
This interface defines that an instance that is of this type contains the tick method, these two implement this interface:
class MyClock implements ClockInterface {
public tick(): void {
console.log("tick");
}
}
let a: ClockInterface = new MyClock();
let b: ClockInterface = {
tick: () => console.log("tick")
}
It's pretty straight forward, as the class implementation is the same as in other OO languages, the 2nd implementation is not as trivial but should be easy to understand for javascript developers.
This works great! but what happens if I want to get a constructor of a class as an argument for my function?
This won't work:
function constructorClock(ctor: ClockInterface): ClockInterface {
return new ctor();
}
The argument here is an instance of ClockInterface and not the class (/the constructor function), so to handle such a scenario we can define an interface for the class itself instead of the instances:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
And now we can have this function:
function constructorClock(ctor: ClockConstructor): ClockInterface {
return new ctor(3, 5);
}
Another issue that these builder interfaces give us is the ability to have definition for the static class members/methods, a good example for this is the ArrayConstructor (which is part of the lib.d.ts):
interface ArrayConstructor {
new (arrayLength?: number): any[];
new <T>(arrayLength: number): T[];
new <T>(...items: T[]): T[];
(arrayLength?: number): any[];
<T>(arrayLength: number): T[];
<T>(...items: T[]): T[];
isArray(arg: any): arg is Array<any>;
prototype: Array<any>;
}
(the definition varies depending on if you're using ES5 or ES6 targets, this is the ES5 one).
As you can see, the interface defines different constructor signatures, the prototype and the isArray function, as you use it like this:
Array.isArray([1,2])
If you did not had the ability to have interfaces for the classes themselves (instead of the instances) then you wouldn't be able to use this isArray function, because this is wrong:
let a = [];
a.isArray(3);
The classes DigitalClock and AnalogClock implement the ClockInterface by having the tick method (that is their instances have this method), but they implement the ClockConstructor interface with their constructor function which is used with new and it returns an instance of ClockInterface.
Hopes this helps clarifying it
Edit
The constructor does not return an interface of course, it returns an instance which implements this ClockInterface interface.
Maybe this will make it easier:
class BaseClock {
protected hour: number;
protected minute: number;
constructor(hour: number, minute: number) {
this.hour = hour;
this.minute = minute;
}
public tick() {
console.log(`time is: ${ this.hour }:${ this.minute }`);
}
}
class DigitalClock extends BaseClock {
constructor(hour: number, minute: number) {
super(hour, minute);
}
tick() {
console.log("digitial");
super.tick();
}
}
class AnalogClock extends BaseClock {
constructor(hour: number, minute: number) {
super(hour, minute);
}
tick() {
console.log("analog");
super.tick();
}
}
interface ClockConstructor {
new (hour: number, minute: number): BaseClock;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): BaseClock {
return new ctor(hour, minute);
}
Instead of an interface, we're now using classes only, does that make more sense?
The syntax: new (hour: number, minute: number): ClockInterface defines a constructor, this:
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
createClock(DigitalClock, 12, 17);
Is like:
function createDigitalClock(hour: number, minute: number): ClockInterface {
return new DigitalClock(hour, minute);
}
createDigitalClock(12, 17);
new ctor(hour, minute); (where ctor is ClockConstructor) is like new DigitalClock(hour, minute) (just more generic).