Published on

Method Overloading Based On Type in TypeScript

Authors

Having worked with a strongly typed language like Java for much of my early career I am used to being able to overload a method based on type. For example, a method that can output the sum of two ints and BigIntegers as Strings would look like:

public String add(int number1, int number2) {
  return (number1 + number2).toString();
}

public String add(BigInteger number1, BigInteger number2) {
  return number1.add(number2).toString();
}

This will unfortunately not work like this in TypeScript:

function add(num1: number, num2: number): string {
  return (num1 + num2).toString()
}

// the below will give an error
function add(num1: BigNumber, num2: BigNumber): string {
  return num1.add(num2).toString()
}

This error makes sense as TypeScript compiles to JavaScript and JavaScript has no types for values.

To illustrate let us see how the above methods would transpile to JavaScript:

function add(num1, num2) {
  return (num1 + num2).toString()
}

function add(num1, num2) {
  return num1.add(num2).toString()
}

As you can see from a method signature perspective the methods look identical and there is no way to differentiate them. JavaScript is pretty forgiving and will have some way of choosing which method to use although this will likely differ based on implementation resulting in weird bugs.

Luckily TypeScript does have the concept of specifying a method parameter as being one or more types. So I could rewrite the original method signature as:

function add(num1: number | BigNumber, num2: number | BigNumber): string {
  //error here **********
  return (num1 + num2).toString()
}

But we will get the error: Operator '+' cannot be applied to types 'number | BigNumber' and 'number | BigNumber'.. To get this to work we need to cast one of these types to the other and work with that type's interface. This is done fairly easily by creating a helper function to smart cast this for us:

function asNumber(num: number | BigNumber): number {
  if ((num as BigNumber).toNumber) {
    return (num as BigNumber).toNumber()
  }
  return num as number
}

This helper function basically uses duck typing to check if num is a BigNumber by checking if it has the toNumber property defined (in this case only BigNumber has this defined).

If it does it casts num to a BigNumber and calls toNumber() on it to convert it to a number. If this duck typing fails it means num is a number already so we simply cast to that and return it.

Seeing how the above transpiles to JavaScript also helps in understanding how this works. This transpiles to:

function asNumber(num) {
  if (num.num) {
    return num.toNumber()
  }
  return num
}

Our original function from earlier can now use this helper function to take either a number or BigNumber or a combination of both as parameters using one method:

function add(num1: number | BigNumber, num2: number | BigNumber): string {
  return (asNumber(num1) + asNumber(num2)).toString()
}

Which transpiles to:

function add(num1, num2) {
  return (asNumber(num1) + asNumber(num2)).toString()
}

The alternative to the above is to have multiple methods doing basically the same thing with different names to differentiate the types of methods e.g. addNumber and addBigNumber. This is not a big deal if you only want to do this for one method but as soon as the number of methods increases this becomes exponentially worse.

This way of method type overloading may not be as clean and elegant as how you would handle this in a language like Java but given the constraints of JavaScript (which TypeScript transpiles to), this seems to be the best compromise.