261

Has anybody done constructor overloading in TypeScript. On page 64 of the language specification (v 0.8), there are statements describing constructor overloads, but there wasn't any sample code given.

I'm trying out a really basic class declaration right now; it looks like this,

interface IBox {    
    x : number;
    y : number;
    height : number;
    width : number;
}

class Box {
    public x: number;
    public y: number;
    public height: number;
    public width: number;

    constructor(obj: IBox) {    
        this.x = obj.x;
        this.y = obj.y;
        this.height = obj.height;
        this.width = obj.width;
    }   

    constructor() {
        this.x = 0;
        this.y = 0;
        this.width = 0;
        this.height = 0;
    }
}

When ran with tsc BoxSample.ts, it throws out a duplicate constructor definition -- which is obvious. Any help is appreciated.


  • as far as I can tell, it doesnt support multiple constructors yet - Johan

10 답변


222

TypeScript allows you to declare overloads but you can only have one implementation and that implementation must have a signature that is compatible with all overloads. In your example, this can easily be done with an optional parameter as in,

interface IBox {    
    x : number;
    y : number;
    height : number;
    width : number;
}

class Box {
    public x: number;
    public y: number;
    public height: number;
    public width: number;

    constructor(obj?: IBox) {    
        this.x = obj && obj.x || 0
        this.y = obj && obj.y || 0
        this.height = obj && obj.height || 0
        this.width = obj && obj.width || 0;
    }   
}

or two overloads with a more general constructor as in,

interface IBox {    
    x : number;
    y : number;
    height : number;
    width : number;
}

class Box {
    public x: number;
    public y: number;
    public height: number;
    public width: number;

    constructor();
    constructor(obj: IBox); 
    constructor(obj?: any) {    
        this.x = obj && obj.x || 0
        this.y = obj && obj.y || 0
        this.height = obj && obj.height || 0
        this.width = obj && obj.width || 0;
    }   
}


  • Actually, it should be possible to let the compiler generate javascript to determine at run-time which overload was taken. But this is unlikely because their philosophy seems te be to generate as little javascript as possible. - remcoder
  • @remcoder, that would always be true. Some kinds of type information are not available at runtime. For example, there is no concept of the interface IBox in the generated JavaScript. It could work for classes and built in types, but I suppose given the potential confusion around this it was omitted. - Drew Noakes
  • Another very important note: while TypeScript is already not typesafe, this further invades it. Function overloading like done here loses any properties that can be checked about the function. The compiler won't care anymore and will assume the returned types to be correct. - froginvasion
  • What makes this not type safe? We're still ensuring that the type is number with public x: number. The safety comes in that we're making sure that parameters, if passed, are of a correct type. - nikk wong
  • @nikkwong froginvasion's point was that using this technique TypeScript doesn't verify the correctness of the overloaded implementation with respect to the overloads. The call sites are verified but the implementation is not. Though not "typesafe", using froginvasion's implied definition, it does limit the code that can be blamed for type errors to the overloaded implementation. - chuckj

72

Note that you can also work around the lack of overloading at the implementation level through default parameters in TypeScript, e.g.:

interface IBox {    
    x : number;
    y : number;
    height : number;
    width : number;
}

class Box {
    public x: number;
    public y: number;
    public height: number;
    public width: number;

    constructor(obj : IBox = {x:0,y:0, height:0, width:0}) {    
        this.x = obj.x;
        this.y = obj.y;
        this.height = obj.height;
        this.width = obj.width;
    }   
}

Edit: As of Dec 5 '16, see Benson's answer for a more elaborate solution that allows more flexibility.


65

Regarding constructor overloads one alternative would be to implement the additional overloads as static factory methods. I think its more readable and less confusing than testing your call arguments. Here is a simple example:

class Person {
    static fromData(data: PersonData) {
        let { first, last, birthday, gender = 'M' } = data 
        return new this(
            `${last}, ${first}`,
            calculateAge(birthday),
            gender
        )
    }

    constructor(
        public fullName: string,
        public age: number,
        public gender: 'M' | 'F'
    ) {}
}

interface PersonData {
    first: string
    last: string
    birthday: string
    gender?: 'M' | 'F'
}


let personA = new Person('Doe, John', 31, 'M')
let personB = Person.fromData({
    first: 'John',
    last: 'Doe',
    birthday: '10-09-1986'
})

Method overloading in TypeScript isn't for real, let's say, as it would require too much compiler-generated code and the core team try to avoid that at all costs. Currently the main reason for method overloading to be present on the language is to provide a way to write declarations for libraries with magic arguments in their API. Since you'll need to do all the heavy-lifting by yourself to handle different sets of arguments I don't see much advantage in using overloads instead of separated methods.


53

Note: this was simplified and updated 4/13/2017 to reflect TypeScript 2.1, see history for TypeScript 1.8 answer.

It sounds like you want the object parameter to be optional, and also each of the properties in the object to be optional. In the example, as provided, overload syntax isn't needed. I wanted to point out some bad practices in some of the answers here. Granted, it's not the smallest possible expression of essentially writing box = { x: 0, y: 87, width: 4, height: 0 }, but this provides all the code hinting niceties you could possibly want from the class as described. This example allows you to call a function with one, some, all, or none of the parameters and still get default values.

 /** @class */
 class Box {
     public x?: number;
     public y?: number;
     public height?: number;
     public width?: number;     

     // The class can work double-duty as the interface here since they are identical
     // Alternately, reference your own interface, e.g.:  `...BoxI = {} as BoxI` 
     constructor(obj: Box = {} as Box) {

         // Define the properties of the incoming `obj` object here. 
         // Setting a default value with the `= 0` syntax is optional for each parameter
         let {
             x = 0,
             y = 0,
             height = 1,
             width = 1
         } = obj;

         //  If needed, make the parameters publicly accessible
         //  on the class ex.: 'this.var = var'.
         /**  Use jsdoc comments here for inline ide auto-documentation */
         this.x = x;
         this.y = y;
         this.height = height;
         this.width = width;
     }
 }

This is a very safe way to write for parameters that may not have all properties of the object defined. You can now safely write any of these:

const box1 = new Box();
const box2 = new Box({});
const box3 = new Box({x:0});
const box4 = new Box({x:0, height:10});
const box5 = new Box({x:0, y:87,width:4,height:0});

 // Correctly reports error in TypeScript, and in js, box6.z is undefined
const box6 = new Box({z:0});  

Compiled, you see that the optional parameters truly are optional, that avoids the pitfalls of a widely used (but error prone) fallback syntax of var = isOptional || default; by checking against void 0, which is shorthand for undefined:

The Compiled Output

var Box = (function () {
    function Box(obj) {
        if (obj === void 0) { obj = {}; }
        var _a = obj.x, x = _a === void 0 ? 0 : _a, _b = obj.y, y = _b === void 0 ? 0 : _b, _c = obj.height, height = _c === void 0 ? 1 : _c, _d = obj.width, width = _d === void 0 ? 1 : _d;
        this.x = x;
        this.y = y;
        this.height = height;
        this.width = width;
    }
    return Box;
}());

Addendum: Setting default values: the wrong way

The || (or) operator

Consider the danger of ||/or operators when setting default fallback values as shown in some other answers. This code below illustrates the wrong way to set defaults. You can get unexpected results when evaluating against falsey values like 0, '', null, undefined, false, NaN:

var myDesiredValue = 0;
var result = myDesiredValue || 2;

// This test will correctly report a problem with this setup.
console.assert(myDesiredValue === result && result === 0, 'Result should equal myDesiredValue. ' + myDesiredValue + ' does not equal ' + result);

Object.assign(this,obj)

In my tests, using es6/typescript destructured object can be almost 90% faster than Object.assign. Using a destructured parameter only allows methods and properties you've assigned to the object. For example, consider this method:

class BoxTest {
    public x?: number = 1;

    constructor(obj: BoxTest = {} as BoxTest) {
        Object.assign(this, obj);
    }
}

If another user wasn't using TypeScript and attempted to place a parameter that didn't belong, say, they might try putting a z property

var box = new BoxTest({x: 0, y: 87, width: 4, height: 0, z: 7});

// This test will correctly report an error with this setup. `z` was defined even though `z` is not an allowed property of obj.
console.assert(typeof box.z === 'undefined')


  • I know this is kind of an old thread but the Ibox casting broke my mind, can you explain it to me how it works? - Nickso
  • I've updated my answer to remove the superfluous casting that was a carryover from coding for Typescript 1.8. The casting that remains is for the empty object ( {} becomes the default object if no parameters are defined; and since {} doesn't validate as a Box, we cast it as a Box. Casting it this way allow us to create a new Box with none of its parameters defined. In your IDE, you can input my example, as well as the const box1 = new Box(); lines, and you can see how casting solves some of the error messages we see in the usage scenarios. - Benson
  • @Benson the BoxTest example contains errors. The TypeScript compiler correctly complains about the wrong usage of the constructor, but the assignment will still occur. The assertion fails because box.z is in fact 7 in your code, not undefined. - Steven Liekens
  • Added a method to the Box class and then the constructor stop working (failed at compile time). Any idea? - JeeShen Lee
  • @JeeShenLee you could either extend the Box class to a newly-named class with methods or create an interface for the parameters expected. The interface type is borrowed from the Box class since classes can act as interfaces. With your added method, the interface was expecting a method to be passed in as part of the object since the class is doing double-duty as the interface. Just copy the first five lines of the Box class and change it to an interface with a new name, such as interface BoxConfig { x?: number ...} and then change the line constructor(obj: BoxConfig = {} as BoxConfig) { - Benson

33

I know this is an old question, but new in 1.4 is union types; use these for all function overloads (including constructors). Example:

class foo {
    private _name: any;
    constructor(name: string | number) {
        this._name = name;
    }
}
var f1 = new foo("bar");
var f2 = new foo(1);


  • Wouldn't the name field also be of type string | number instead of any? - Carcigenicate
  • You could definitely do that, yes, and it might be a bit more consistent, but in this example, it'd only give you access to .toString() and .valueOf(), in Intellisense, so for me, using any is just fine, but to each his/her own. - Joe

20

Update (8 June 2017): guyarad and snolflake make valid points in their comments below to my answer. I would recommend readers look at the answers by Benson, Joe and snolflake who have better answers than mine.

Original Answer (27 January 2014)

Another example of how to achieve constructor overloading:

class DateHour {

  private date: Date;
  private relativeHour: number;

  constructor(year: number, month: number, day: number, relativeHour: number);
  constructor(date: Date, relativeHour: number);
  constructor(dateOrYear: any, monthOrRelativeHour: number, day?: number, relativeHour?: number) {
    if (typeof dateOrYear === "number") {
      this.date = new Date(dateOrYear, monthOrRelativeHour, day);
      this.relativeHour = relativeHour;
    } else {
      var date = <Date> dateOrYear;
      this.date = new Date(date.getFullYear(), date.getMonth(), date.getDate());
      this.relativeHour = monthOrRelativeHour;
    }
  }
}

Source: http://mimosite.com/blog/post/2013/04/08/Overloading-in-TypeScript


  • This isn't a constructive comment - but, wow, this is ugly. Kind of misses the point of type safety in TypeScript... - guyarad
  • Is that a constructor overload?! No thanks! I'd rather implement a static factory method for that class, really ugly indeed. - cvsguimaraes
  • I suppose today we could have dateOrYear: Date | number, - Pascal Ganaye

3

In the case where an optional, typed parameter is good enough, consider the following code which accomplishes the same without repeating the properties or defining an interface:

export class Track {
   public title: string;
   public artist: string;
   public lyrics: string;

   constructor(track?: Track) {
     Object.assign(this, track);
   }
}

Keep in mind this will assign all properties passed in track, eve if they're not defined on Track.


1

Another version like to @ShinNoNoir's code, using default values and spread syntax:

class Box {
    public x: number;
    public y: number;
    public height: number;
    public width: number;

    constructor({x, y, height, width}: IBox = { x: 0, y: 0, height: 0, width: 0 }) {
        this.x = x;
        this.y = y;
        this.height = height;
        this.width = width;
    }
}


  • In that case, wouldn't it be better to type the parameter as {} instead of IBox? You're already enumerating the property constraints... - Roy Tinker

0

You can handle this by :

import { assign } from 'lodash'; // if you don't have lodash use Object.assign
class Box {
    x: number;
    y: number;
    height: number;
    width: number;
    constructor(obj: Partial<Box> = {}) {    
         assign(this, obj);
    }
}

Partial will make your fields (x,y, height, width) optionals, allowing multiple constructors

eg: you can do new Box({x,y}) without height, and width.

The = {} will handle falsy value such as undefined, null etc, and then you can do new Box()


-4

You should had in mind that...

contructor()

constructor(a:any, b:any, c:any)

It's the same as new() or new("a","b","c")

Thus

constructor(a?:any, b?:any, c?:any)

is the same above and is more flexible...

new() or new("a") or new("a","b") or new("a","b","c")

Linked


Latest