beNerd
beNerd

Reputation: 3374

Understanding Constructor Interfaces in typescript

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

Answers (1)

Nitzan Tomer
Nitzan Tomer

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


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).

Upvotes: 4

Related Questions