HOWTO · TypeScript

How to Overload Constructors in TypeScript

Learn how TypeScript constructor overload signatures define accepted calls and share one implementation.

On this page

TypeScript supports constructor overloading through multiple overload signatures and one implementation. The signatures describe the argument shapes callers may use; they do not create multiple constructors at runtime. Use overloads when a class has a small set of distinct initialization forms, and make the shared implementation handle every form those signatures allow.

How TypeScript Constructor Overloads Work

Write each public overload signature before the implementation signature. Call sites are checked against the overload list. The implementation signature must be broad enough to accept the declared forms, but it is hidden from callers and does not add another accepted form. This distinction helps keep the class API intentional: a value fitting the implementation’s union types can still be rejected if no overload describes that combination.

Declare Overload Signatures and One Implementation

This class supports three call shapes: no arguments, a country name, or an apple count with a banana count and fruit label. In TypeScript, declare those shapes as constructor();, constructor(country: string);, and constructor(apples: number, bananas: number, fruit: string);, then provide one implementation whose parameters can represent all three forms. The implementation uses optional parameters and a union for its first argument, narrowing that union with typeof.

The typed implementation signature can be written as constructor(countryOrApples?: string | number, bananas?: number, fruit?: string). Place it after the three overload declarations inside the class. In its body, test typeof countryOrApples === "string" and typeof countryOrApples === "number" to narrow the union before using it; the remaining branch handles the no-argument call. This is TypeScript syntax guidance; the executable example below uses JavaScript so it can demonstrate the same runtime behavior without relying on a TypeScript compiler configuration.

Run this JavaScript example to see the shared implementation’s runtime behavior:

class Fruits {
  constructor(countryOrApples, bananas, fruit) {
    if (typeof countryOrApples === "string") {
      console.log(`Apple country: ${countryOrApples}`);
    } else if (typeof countryOrApples === "number") {
      console.log(`Apple count: ${countryOrApples}`);
    } else {
      console.log("Using default values");
    }

    this.numBananas = bananas ?? 0;
    this.unknownFruit = fruit ?? "";
  }
}

new Fruits();
new Fruits("India");
new Fruits(8, 4, "Orange");
Using default values
Apple country: India
Apple count: 8

Optional parameters and unions describe what the implementation can receive; they do not make every combination valid at the TypeScript call site. The overload signatures accept no arguments, one string, or three arguments with the specified types. If an implementation uses a union for fruit, it must still branch or validate values when different combinations require different behavior.

Check Arguments and Handle Runtime Behavior

new Fruits("India", 4, "Orange") is rejected because it matches none of the three overload signatures, even though the implementation parameters can represent those values. The diagnostic is a TypeScript compile-time check; it is not a separate runtime constructor error. Keep overloads aligned with combinations the implementation actually supports, and perform any necessary narrowing before using a value as a particular type.

Removing the overload declarations exposes the implementation signature to callers. That may allow extra argument combinations, such as a string country together with a fruit label, without ensuring the implementation gives that combination the intended meaning. Broaden the public shape only when the implementation deliberately handles every newly accepted combination. An implementation signature is therefore both an internal parameter contract and a reminder to handle each supported branch.

When to Use a Static Factory Instead

A static factory can give distinct initialization paths names, such as Fruits.fromCountry("India") or Fruits.fromCounts(8, 4). Each method can validate its own inputs and then call a simple constructor. This is often clearer when overloads encode different meanings or require substantially different validation. It also makes call sites more descriptive.

Factories add method names and may be unnecessary when there are only a few obvious argument shapes. Choose them when named intent improves readability or when each path has its own rules; retain constructor overloads when construction remains straightforward and the call forms are easy to distinguish. Both approaches should make the accepted inputs explicit and keep runtime handling consistent with that public API.