Reputation: 3374
I am new to typescript and I am stuck in understanding constructor interfaces and the way they are type checked. Here is a snippet from docs:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
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");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Here is what doc says about the above code:
Because createClock’s first parameter is of type ClockConstructor, in createClock(AnalogClock, 7, 32), it checks that AnalogClock has the correct constructor signature.
Now this essentially means that DigitalClock class or AnalogClock classes have the type defined by ClockConstructor interface. How? It's a class and interface is describing a constructor.
Any takers please?
Upvotes: 4
Views: 2944
Reputation: 164139
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
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).
Upvotes: 4