Let's begin with the straightforward interface provided in your example:
interface ClockInterface {
tick();
}
This interface states that any instance of this type must have the tick
method. The following two classes 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")
}
The implementation is clear, resembling other object-oriented languages for the class and offering a more unique approach for the 2nd implementation that may be easier to grasp for JavaScript developers.
While this setup works effectively, what if one desires to pass a constructor of a class as an argument for a function? This straightforward attempt won't suffice:
function constructorClock(ctor: ClockInterface): ClockInterface {
return new ctor();
}
The issue here lies in expecting an instance of ClockInterface
rather than the actual class or constructor function. To address this scenario, we can define an interface specifically for the class itself rather than its instances:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
With this, a revised function becomes feasible:
function constructorClock(ctor: ClockConstructor): ClockInterface {
return new ctor(3, 5);
}
An additional advantage of using these builder interfaces is the ability to define static class members/methods. A prime exemplar is the ArrayConstructor
:
interface ArrayConstructor {
// Various constructor signatures along with prototype methods like isArray
}
This interface showcases multiple constructor signatures alongside supporting functions like isArray
. Without the capacity to have interfaces for classes themselves instead of their instances, utilizing such functions would prove cumbersome.
In conclusion, while DigitalClock
and AnalogClock
adhere to implementing ClockInterface
, they also conform to the ClockConstructor
interface via their constructor function used with new
.
I hope this elucidates some aspects better.
Edit
To clarify, the constructor doesn't return an interface
but an instance adhering to the ClockInterface
. To simplify further:
class BaseClock {
// Class implementations
}
// More classes extending BaseClock
interface ClockConstructor {
new (hour: number, minute: number): BaseClock;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): BaseClock {
return new ctor(hour, minute);
}
Shifting from an interface to pure classes may streamline comprehension. Would you find this approach more coherent?
The synopsis:
new (hour: number, minute: number): ClockInterface
denotes a constructor, akin to:
createClock(DigitalClock, 12, 17);
This mirrors:
function createDigitalClock(hour: number, minute: number): ClockInterface {
return new DigitalClock(hour, minute);
}
createDigitalClock(12, 17);
The abstraction new ctor(hour, minute);
(with ctor
being ClockConstructor
) equates to new DigitalClock(hour, minute)
, albeit in a more generalized manner.