Understand TypeScript Constructor Overloading

When I first started working with TypeScript classes, I was searching for solutions to multiple ways of constructing an object in TypeScript, similar to those in Java. After searching, I discovered that TypeScript doesn’t support traditional constructor overloading. However, with an alternative approach that utilizes multiple constructor signatures and a single implementation, we can achieve the same output.

In this TypeScript tutorial, I’ll explain how constructor overloading works in TypeScript, with the help of real-time examples.

What is Constructor Overloading in TypeScript?

Constructor overloading is a feature that allows a class to have multiple constructor declarations with different parameter types or counts. This gives you flexibility in how objects can be created.

Unlike languages like C# or Java, TypeScript implements constructor overloading through a combination of declaration signatures and a single implementation.

Here’s a simple example of what constructor overloading looks like:

class Username {
  name: string;
  age: number;
  email?: string;

  // Constructor overloads
  constructor(name: string);
  constructor(name: string, age: number);
  constructor(name: string, age: number, email: string);

  // Single constructor implementation
  constructor(name: string, age?: number, email?: string) {
    this.name = name;
    this.age = age ?? 0;
    this.email = email;
  }

Test the method:

  // Method to return user details as a string
  getInfo(): string {
    return `Name: ${this.name}\nAge: ${this.age}\nEmail: ${this.email ?? 'N/A'}`;
  }

  // Method to return user details as an object
  toObject(): { name: string; age: number; email?: string } {
    return {
      name: this.name,
      age: this.age,
      email: this.email
    };
  }
}

const user1 = new Username("Alice");
const user2 = new Username("Bob", 28);
const user3 = new Username("Charlie", 35, "[email protected]");

// Print string output
console.log(user1.getInfo());
console.log(user2.getInfo());
console.log(user3.getInfo());

console.log("-----");

// Print object output
console.log(user1.toObject());
console.log(user2.toObject());
console.log(user3.toObject());

Output:

Overload Constructor in TypeScript

Check out: Iterate Over Objects in TypeScript

Method 1: Basic Constructor Overloading

The simplest way to implement constructor overloading is to define multiple constructor signatures, followed by a single implementation constructor that handles all cases.

Here’s how I usually implement basic constructor overloading:

class Product {
  id: number;
  name: string;
  price: number;
  category?: string;

  // Overload signatures
  constructor(id: number, name: string, price: number);
  constructor(id: number, name: string, price: number, category: string);

  // Implementation
  constructor(id: number, name: string, price: number, category?: string) {
    this.id = id;
    this.name = name;
    this.price = price;
    this.category = category;

    console.log("Product created:");
    console.log(`  ID: ${this.id}`);
    console.log(`  Name: ${this.name}`);
    console.log(`  Price: $${this.price}`);
    console.log(`  Category: ${this.category ?? "N/A"}`);
    console.log("--------------------");
  }
}

// Usage examples
const laptop = new Product(1, "MacBook Pro", 1999);
const phone = new Product(2, "iPhone 13", 999, "Electronics");

Output:

Constructor Overloading in TypeScript

This approach works well for simple cases where you have optional parameters at the end of your parameter list.

Method 2: Constructor Overloading with Different Parameter Types

Sometimes you need to accept completely different parameter types in different constructors. Here’s how I handle this in my projects:

class Payment {
  amount: number;
  currency: string;
  date: Date;

  // Overload signatures
  constructor(amount: number);
  constructor(amount: number, currency: string);
  constructor(paymentData: { amount: number; currency: string; date: Date });

  // Implementation
  constructor(
    amountOrData: number | { amount: number; currency: string; date: Date },
    currency?: string
  ) {
    if (typeof amountOrData === 'object') {
      this.amount = amountOrData.amount;
      this.currency = amountOrData.currency;
      this.date = amountOrData.date;

      console.log("Payment created using object input:");
    } else {
      this.amount = amountOrData;
      this.currency = currency || 'USD';
      this.date = new Date();

      console.log("Payment created using individual values:");
    }

    console.log(`  Amount: ${this.amount}`);
    console.log(`  Currency: ${this.currency}`);
    console.log(`  Date: ${this.date.toDateString()}`);
    console.log("--------------------");
  }
}

// Usage examples
const payment1 = new Payment(99.99);
const payment2 = new Payment(149.99, "EUR");
const payment3 = new Payment({
  amount: 299.99,
  currency: "GBP",
  date: new Date(2025, 6, 15) // Note: July (0-based month index)
});

Output:

Overload Constructor with parameter in TypeScript

This pattern is particularly useful for complex objects where you want to provide both individual parameters and a configuration object option.

Check out: TypeScript Generic Object Types

Method 3: Factory Methods as an Alternative

Sometimes, traditional constructor overloading can become complex and hard to maintain. In these cases, I often turn to static factory methods:

class ShoppingCart {
  items: Array<{product: string, quantity: number, price: number}>;
  private constructor(items: Array<{product: string, quantity: number, price: number}> = []) {
    this.items = items;
  }

  // Factory methods instead of constructor overloading
  static empty(): ShoppingCart {
    return new ShoppingCart();
  }

  static withItem(product: string, price: number): ShoppingCart {
    return new ShoppingCart([{product, quantity: 1, price}]);
  }

  static fromJSON(json: string): ShoppingCart {
    const data = JSON.parse(json);
    return new ShoppingCart(data.items);
  }

  // Add methods to the class
  addItem(product: string, quantity: number, price: number): void {
    this.items.push({product, quantity, price});
  }

  getTotal(): number {
    return this.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
  }
}

// Usage examples
const emptyCart = ShoppingCart.empty();
const cartWithItem = ShoppingCart.withItem("American Football", 49.99);
const cartFromJSON = ShoppingCart.fromJSON('{"items":[{"product":"Baseball Cap","quantity":2,"price":24.99}]}');

Output:

Factory methods in TypeScript overloading

I find this approach particularly useful for complex initialization logic or when constructor parameters could be ambiguous.

Check out: Use Try Catch in TypeScript

Advanced Constructor Overloading Patterns

After years of TypeScript development, I’ve developed some advanced patterns for constructor overloading that work well in larger applications:

Handling Multiple Constructor Types with Union Types

class Address {
  street: string;
  city: string;
  state: string;
  zipCode: string;

  // Overload signatures
  constructor(fullAddress: string);
  constructor(street: string, city: string, state: string, zipCode: string);

  // Implementation
  constructor(streetOrFullAddress: string, city?: string, state?: string, zipCode?: string) {
    if (!city && !state && !zipCode) {
      console.log("Creating address from full string...");
      // Parse full address format: "123 Main St, San Francisco, CA 94107"
      const parts = streetOrFullAddress.split(',').map(part => part.trim());
      this.street = parts[0] || '';
      this.city = parts[1] || '';
      const stateZip = (parts[2] || '').split(' ');
      this.state = stateZip[0] || '';
      this.zipCode = stateZip[1] || '';

      console.log("Parsed full address:");
    } else {
      console.log("Creating address from individual parts...");
      this.street = streetOrFullAddress;
      this.city = city || '';
      this.state = state || '';
      this.zipCode = zipCode || '';
    }

    console.log(`  Street: ${this.street}`);
    console.log(`  City: ${this.city}`);
    console.log(`  State: ${this.state}`);
    console.log(`  Zip Code: ${this.zipCode}`);
    console.log("--------------------");
  }

  toString(): string {
    return `${this.street}, ${this.city}, ${this.state} ${this.zipCode}`;
  }
}

// === Usage Examples ===
const address1 = new Address("123 Main St", "San Francisco", "CA", "94107");
const address2 = new Address("123 Main St, San Francisco, CA 94107");

console.log("Address 1:", address1.toString());
console.log("Address 2:", address2.toString());

Output:

Advance constructor overloading in TypeScript

Using Discriminated Unions for Complex Constructors

type UserProps = 
  | { type: 'basic'; name: string; email: string } 
  | { type: 'social'; name: string; socialId: string; provider: 'Google' | 'Facebook' }
  | { type: 'guest'; guestId: string };

class User {
  name: string;
  id: string;
  accountType: 'basic' | 'social' | 'guest';

  constructor(props: UserProps) {
    switch(props.type) {
      case 'basic':
        this.name = props.name;
        this.id = `user_${Math.random().toString(36).substring(2, 9)}`;
        this.accountType = 'basic';
        break;
      case 'social':
        this.name = props.name;
        this.id = `${props.provider.toLowerCase()}_${props.socialId}`;
        this.accountType = 'social';
        break;
      case 'guest':
        this.name = 'Guest User';
        this.id = props.guestId;
        this.accountType = 'guest';
        break;
    }
  }
}

// Usage examples
const basicUser = new User({ type: 'basic', name: 'John Doe', email: '[email protected]' });
const socialUser = new User({ type: 'social', name: 'Jane Smith', socialId: '123456789', provider: 'Google' });
const guestUser = new User({ type: 'guest', guestId: 'guest_abc123' });

This pattern works exceptionally well when you have fundamentally different ways to construct an object, each requiring its own set of parameters.

Check out: Set Default Values in TypeScript Interfaces

Best Practices for Constructor Overloading

Through my years of working with TypeScript, I’ve developed these best practices for constructor overloading:

  1. Keep it simple: Only use overloading when it genuinely improves API usability.
  2. Use JSDoc comments: Document each overload signature to help IDE users understand the different options.
  3. Consider factory methods: For complex initialization logic, static factory methods are often clearer than constructor overloads.
  4. Avoid type assertion: Try to structure your implementation to avoid using as type assertions.
  5. Test all overloads: Ensure all constructor variations work as expected with dedicated tests.

Constructor overloading is a powerful TypeScript feature that can make your APIs more flexible and intuitive. By following these patterns and best practices, you can create classes that are both type-safe and easy to use.

I hope that, by following the above examples and methods, you have understood the concept of Constructor overloading in TypeScript.

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.