Skip to content
Alexander Opalic
Alexander Opalic

TIL: TypeScript Function Overloads

Today I learned about function overloads in TypeScript. They solve a common problem - making your code understand exactly what type comes out based on what type goes in.

What Function Overloads Do

Function overloads let me define multiple ways to call the same function:

// These tell TypeScript about different ways to call my function
function function convert(value: string): number (+1 overload)convert(value: stringvalue: string): number;
function function convert(value: number): string (+1 overload)convert(value: numbervalue: number): string;

// This is the actual function that runs
function function convert(value: string): number (+1 overload)convert(value: string | numbervalue: string | number): string | number {
  if (typeof value: string | numbervalue === "string") {
    return function parseInt(string: string, radix?: number): number
Converts a string to an integer.
@paramstring A string to convert into a number.@paramradix A value between 2 and 36 that specifies the base of the number in `string`. If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. All other strings are considered decimal.
parseInt
(value: stringvalue);
} else { return value: numbervalue.Number.toString(radix?: number): string
Returns a string representation of an object.
@paramradix Specifies a radix for converting numeric values to strings. This value is only used for numbers.
toString
();
} } const const a: numbera = function convert(value: string): number (+1 overload)convert("42"); // TypeScript knows this is a number const const b: stringb = function convert(value: number): string (+1 overload)convert(42); // TypeScript knows this is a string

How This Improves My Code

Without overloads, I’d have to use a union type, which loses information:

function function convert(value: string | number): string | numberconvert(value: string | numbervalue: string | number): string | number {
  if (typeof value: string | numbervalue === "string") {
    return function parseInt(string: string, radix?: number): number
Converts a string to an integer.
@paramstring A string to convert into a number.@paramradix A value between 2 and 36 that specifies the base of the number in `string`. If this argument is not supplied, strings with a prefix of '0x' are considered hexadecimal. All other strings are considered decimal.
parseInt
(value: stringvalue);
} else { return value: numbervalue.Number.toString(radix?: number): string
Returns a string representation of an object.
@paramradix Specifies a radix for converting numeric values to strings. This value is only used for numbers.
toString
();
} } const const a: string | numbera = function convert(value: string | number): string | numberconvert("42"); // TypeScript only knows this is string | number const const b: string | numberb = function convert(value: string | number): string | numberconvert(42); // TypeScript only knows this is string | number

With overloads, TypeScript remembers which type I get back based on what I put in!

Overloads vs. Union Types

  • Overloads: More precise typing, TypeScript knows exactly which output type matches which input type
  • Union types: Simpler code, but TypeScript only knows all possible output types, not which specific one

Limitations in TypeScript

The big drawback? In TypeScript, I can’t write separate code for each overload. I write one function that handles all cases. This gets messy with complex functions.

C++ does this better. In C++, each overload is a completely separate function:

// Two totally separate functions
void convert(int value) {
    // Code for integers only
    cout << to_string(value) << endl;
}

void convert(string value) {
    // Different code for strings only
    cout << stoi(value) << endl;
}

In TypeScript, I have to write my own type-checking logic to figure out which version was called. This makes overloads in TypeScript harder to use for complex cases.

#typescript