Overloading TypeScript Constructors like C++ or Java with a Catch

Aug 31, 2023
Whenever I was looking into the TypeScript Date class constructor implementation, I always wondered how its constructor signature worked like that, how it can have many signature, how can it work with 1st argument as number or string or an instance of itself.

Table of Content

Click to expand

Whenever I was looking into the TypeScript Date class constructor implementation, I always wondered how its constructor signature worked like that, how it can have many signature, how can it work with 1st argument as number or string or an instance of itself. Every time I tried to implement the constructor signature in TypeScript using the documentation like TypeScript says I was getting type error shown below.

I was working on node-nepali-datetime open source package in github.You might be thinking now that this is reinventing the wheel. The reason is that Nepal is around 60 years into the future. The first month of Nepalse calender, the Bikram Sambat calender, is Baisakh. It falls somewhere around April and has other similar features like the English (A.D) calender.

But whenever I looked into typescript implementation for date. I could see something that there were 5 different overload for date.

In the fifth constructor implementation, I obeserved that the typescript doc string can be the reason behind this implementation then I tried putting doc string in places.

    /**
     * Creates NepaliDate instance from current date
     * @example new Date()
     */
    constructor()

    /**
     * Create NepaliDate instance from provided english date
     *
     * @param {Date} date
     *
     * @example
     * const nepaliDate = new NepaliDate( new Date('2020-01-01') )
     *
     */
    constructor(date: Date)

    /**
     * Create NepaliDate instance from using a provided english date
     * @param {NepaliDate} date: nepali date value provided as a value.
     * @example
     * const nepaliDateOld = new NepaliDate('2080-01-01')
     * const nepaliDate = new NepaliDate( nepaliDateOld )
     *
     */
    constructor(date: NepaliDate)

    /**
     * Create NepaliDate instance by parsing a provided string value
     * @param {string} value: string date time.
     * @example
     * const nepaliDate = new NepaliDate('2080-01-01')
     *
     */
    constructor(value: string)

    /**
     * Create NepaliDate instance by parsing a provided numeric value
     * @param {number} value: numeric value
     * @example
     * const n = new NepaliDate(new Date(373314600000))
     *
     */
    constructor(value: number)

    /**
     * Create NepaliDate instance by parsing a provided time string with format provided
     * @param dateString: string date time
     * @param format: string format of the date provided
     * @example
     * const n1 = new NepaliDate('2042/08/12 14-05-23.789', 'YYYY/MM/DD HH-mm-ss.SSS')
     *
     */
    constructor(dateString: string, format: string)

    /**
     * Creates a new instance of the Date class.
     *
     * @constructor
     * @param {number} year - The year.
     * @param {number} month - The month (0-11, where 0 is January and 11 is December).
     * @param {number=} day - The day of the month (1-31).
     * @param {number=} hour - The hour of the day (0-23).
     * @param {number=} minute - The minute (0-59).
     * @param {number=} second - The second (0-59).
     * @param {number=} ms - The milliseconds (0-999).
     *
     * @example
     * const [year, month, day] = [2080, 1, 12]
     * const hour = 12
     * const minute = 30
     * const second = 45
     * const ms = 500
     * const nd = new NepaliDate(year, month, day, hour, minute, second, ms)
     */
    constructor(        year: number,
        month: number,
        day?: number,
        hour?: number,
        minute?: number,
        second?: number,
        ms?: number    ) {}

Unforunately, the same type trouble still persisted, I tried looking into various ways of generalizing many overloads into a final constructor. Before my PR, the project had the following implementation

constructor(...args: any[]) {
        if (args.length === 0) {
            this._setDateObject(new Date())
        } else if (args.length === 1) {
            const e = args[0]
            if (typeof e === 'object') {
                if (e instanceof Date) {
                    this._setDateObject(e)
                } else if (e instanceof NepaliDate) {
                    this.timestamp = e.timestamp
                    this.year = e.year
                    this.yearEn = e.yearEn
                    this.month = e.month
                    this.monthEn = e.monthEn
                    this.day = e.day
                    this.dayEn = e.dayEn
                    this.hour = e.hour
                    this.minute = e.minute
                    this.weekDay = e.weekDay
                } else {
                    throw new Error('Invalid date argument')
                }
            } else if (typeof e === 'number') {
                this._setDateObject(new Date(e))
            } else if (typeof e === 'string') {
                // Try to parse the date
                this.set.apply(this, parse(e))
            } else {
                throw new Error('Invalid date argument')
            }
        } else if (
            args.length === 2 &&
            typeof args[0] === 'string' &&
            typeof args[1] === 'string'
        ) {
            const [dateTimeString, format] = args
            this.set.apply(this, parseFormat(dateTimeString, format))
        } else {
            this.set(
                args[0], // year
                args[1], // month
                args[2] ?? 1, // day
                args[3] ?? 0, // hour
                args[4] ?? 0, // minute
                args[5] ?? 0, // second
                args[6] ?? 0 // ms
            )
        }
    }

The above implemention is almost perfect but, then friendly neighbour hood eslint yelled about using any. I even considered making an exception to the rule using the following.

@typescript-eslint/no-explicit-any

But this is an open source project and I was not bound by time limit or pressured to delievered fast and no one was depending on me for this. So, I decided to dive more deep into working of typescript and constructor.

Luckily, javascript has an awesome feature called arguments. I tried to look into it to find out if it could be more helpful. This is how arguments work. I was a bit skeptical that it would throw a type error like other implementation but to my suprise it didn’t raise any type error in the editor.

function func1(a, b, c) {
  console.log(arguments)
  // Excepted output: 1, 2, 3

  console.log(arguments\[0\]);
  // Expected output: 1

  console.log(arguments\[1\]);
  // Expected output: 2

  console.log(arguments\[2\]);
  // Expected output: 3
}

func1(1, 2, 3);

The final constructor code looks something like this. No type errors are thrown and the code supports all the types correclty. After adding some typescript doc string. This code was good to go for me. But there is a catch.

    constructor()
    constructor(date: Date)
    constructor(date: NepaliDate)
    constructor(value: string)
    constructor(value: number)
    constructor(dateString: string, format: string)
    constructor(        year: number,
        month: number,
        day?: number,
        hour?: number,
        minute?: number,
        second?: number,
        ms?: number    )
    constructor() {
        if (arguments.length === 0) {
            this.\_setDateObject(new Date())
        } else if (arguments.length === 1) {
            const e = arguments\[0\]
            if (typeof e === 'object') {
                if (e instanceof Date) {
                    this.\_setDateObject(e)
                } else if (e instanceof NepaliDate) {
                    this.timestamp = e.timestamp
                    this.year = e.year
                    this.yearEn = e.yearEn
                    this.month = e.month
                    this.monthEn = e.monthEn
                    this.day = e.day
                    this.dayEn = e.dayEn
                    this.hour = e.hour
                    this.minute = e.minute
                    this.weekDay = e.weekDay
                } else {
                    throw new Error('Invalid date argument')
                }
            } else if (typeof e === 'number') {
                this.\_setDateObject(new Date(e))
            } else if (typeof e === 'string') {
                // Try to parse the date
                this.set.apply(this, parse(e))
            } else {
                throw new Error('Invalid date argument')
            }
        } else if (
            arguments.length === 2 &&
            typeof arguments\[0\] === 'string' &&
            typeof arguments\[1\] === 'string'
        ) {
            const \[dateTimeString, format\] = arguments
            this.set.apply(this, parseFormat(dateTimeString, format))
        } else {
            this.set(
                arguments\[0\], // year
                arguments\[1\], // month
                arguments\[2\] ?? 1, // day
                arguments\[3\] ?? 0, // hour
                arguments\[4\] ?? 0, // minute
                arguments\[5\] ?? 0, // second
                arguments\[6\] ?? 0 // ms
            )
        }
    }

The catch is that in the actual implemenation of the code where things gets executed, the types information is completely lost resulting in code that checks for every single possible combination of values among the many implemenation using typeof, instanceof and arguments.length combination. The Date code in the javascript is native C++ code, so it was not such helpful for me. But with proper doc string added this code from user prespective is close to the native Date constructor implementation as I can go.

I took this design descision and I think this really works for now, but if you have any suggestion on why this peice of code sucks or is not good. Let me know. I like to write about technology and programming in linkedin. So, lets get connected there. Here are my socials.

https://www.linkedin.com/in/nirjalpaudel/

n1rjal - Overview _Curious about Tech | Nodejs 🩵 | Database 🔥 . n1rjal has 83 repositories available. Follow their code on GitHub._github.com

Related Posts

tips and tricks

I Bet You Don’t Use These JavaScript Tricks and Practices

Tell me how many did you know by responding to the article and you may share this with your network to find out how many they know.
I Bet You Don’t Use These JavaScript Tricks and Practices

You may also like

database

Eat the frog: MongoDB Design Patterns

Here are some of the few design patterns that you can use to better your MongoDB schema, so you’ll keep your team happy and yourself happier.
Eat the frog: MongoDB Design Patterns
database

Best way to count documents in MongoDB

Hello everyone 👋👋 I created a MongoDB database with 10 million documents.😄 My schema is simple and is for a very very basic inventory management system.
Best way to count documents in MongoDB
productivity

Vim Selection Groups

In Neovim, the scope that regular expression select group is out of this world. Here is a screencast of me making a json object out of just text data.
Vim Selection Groups
productivity

My journey to VIM, from 😕 to 😄

Vim has been throne till my software development. Something that I always ignored and looked away from. Here I would love to reverse time a by few years and see how my programming journey went and vim has made the journey better making me a more productive software enginner overall.
My journey to VIM, from 😕 to 😄
Reading List Contact
server

© Nirjal Paudel