25
votes

I received the following interface from a library that I'm using:

export interface LatLng {
  constructor(lat: number, lng: number): void;
  lat(): number;
  lng(): number;
}

How do I create an implementation of this class? (I need a testing mock) A natural-looking implementation with constructor defined as:

export class LatLngImpl implements LatLng {
  constructor(private _lat: number, private _lng: number) {  }

doesn't compile:

Class 'LatLngImpl' incorrectly implements interface 'LatLng'. Types of property 'constructor' are incompatible. Type 'Function' is not assignable to type '(lat: number, lng: number) => >void'. Type 'Function' provides no match for the signature '(lat: number, lng: >number): void'

I read about contructor-interfaces in typescript, but I don't think it's applicable here.

Edit:

What I don't undertand is this constructor() declaration in the interface. The interfaces with constructor signatures use new () syntax.

3
More formally, a class implementing an interface is a contract on what an instance of the class has. Since an instance of a class won't contain a construct signature, it cannot satisfy the interface. – Ryan Cavanaugh stackoverflow.com/questions/13407036/…Per Svensson
Yes, I understand how ` new () : MyInterface` works. But in that library, it's declared as "constructor(): void". I suspect this is some kind of mistake. The declaration in the interface treats it as a normal function, while in classes, constructor is a reserved word.Mateusz Stefek

3 Answers

18
votes

Where is this library from? Whoever wrote it deserves a stern talking-to. Either it's a mistake (in which case they never tested it) or it's intentional but eeeeevil: using the not-quite-reserved word constructor as an identifier for an instance method is just asking for trouble.

EDIT 2019-04-25: The trouble is worse than just "this is a bad idea"; it actually looks like once JavaScript natively supports class fields, it is going to be an error to have a class with a property named "constructor". TypeScript will change to reflect this in version 3.5+, so the implementation below will stop working then. See recently opened GitHub issue Microsoft/TypeScript#31020 for more information. After TS3.5 it looks like there will be no way to have a class with an instance constructor property that is not the actual constructor function itself.

Either the library author intended to refer to a real constructor that you call with new, in which case they should do something like @Series0ne's suggestion; or the author really wants to use an instance method to initialize the object, in which case they should do everyone a favor and use a more conventional method name like init().

In either case nobody should accept the interface you've been given, and certainly nobody should implement it.

Let's implement it:

export class LatLngImpl implements LatLng {
  private _lat: number;
  private _lng: number;
  lat() {
    return this._lat;
  }
  lng() {
    return this._lng;
  }
  // here's the important part, but see Microsoft/TypeScript#31020
  "constructor"(lat: number, lng: number) { 
    this._lat = lat;
    this._lng = lng;
  }
}

The trick is to use the string literal "constructor" instead of the bare word constructor. From the TypeScript spec:

String literals may be used to give properties names that are not valid identifiers

By using the string literal, we were able to declare it as an instance method, and not the static class constructor method that is invoked when you call new, and it compiles happily.

Now we can use it, if we dare:

const latLng = new LatLngImpl();
latLng.constructor(47.6391132, -122.1284311); // 😠 Why not init()?
console.log("Latitude: " + latLng.lat() + ", Longitude: " + latLng.lng());

Yuck, we shouldn't have dared.

EDIT AGAIN 2019-04-25 The above quoted-string implementation will not work starting in TypeScript 3.5, and the correct answer to this will be "you can't do this" and "use a real constructor instead".

Hope that helps; good luck!

15
votes

A constructor is technically a special, static function call that returns an instance of itself, so it doesn't really make sense for it to be part of an interface, because interface members are instance bound.

Typescript has a bit of a compiler trick to make them statically bound, and uses this for ambient declarations.

In your case, to provide an implementation, you need to remove the constructor from the interface:

interface LatLng {
    lat(): number;
    lng(): number;
}

class LatLngImpl implements LatLng {
    constructor(lat: number, lng: number) {

    }

    lat(): number {
        return 0;
    }

    lng(): number {
        return 0;
    }
}

If LatLng is implemented elsewhere, you simply need to provide an ambient declaration:

interface LatLng {
    lat(): number;
    lng(): number;
}

interface LatLngConstructor {
    new(lat: number, lng: number): LatLng;
}

declare var LatLng: LatLngConstructor;

Notice that LatLngConstructor defines a new(...): LatLng, which is what describes the constructor signature.

0
votes

I believe you cannot put constructor inside an interface.

interface IPerson 
{
    firstName: string;
    lastName: string;
}

class Person implements IPerson 
{
    constructor (public firstName: string, public lastName: string) 
    {
        //TODO
    }
}

var personA: IPerson = new Person('Jane', 'Smith');
var personB: IPerson = { firstName: 'Jo', lastName: 'Smith' };