Question

TypeScript Constructor Overloading: How to Use Overload Signatures Correctly

typescriptconstructoroverloading

Question

I want to use constructor overloading in TypeScript. The language specification mentions constructor overloads, but I could not find a clear example.

I am experimenting with a simple class like this:

interface IBox {
    x: number;
    y: number;
    height: number;
    width: number;
}

class Box {
    public x: number;
    public y: number;
    public height: number;
    public width: number;

    constructor(obj: IBox) {
        this.x = obj.x;
        this.y = obj.y;
        this.height = obj.height;
        this.width = obj.width;
    }

    constructor() {
        this.x = 0;
        this.y = 0;
        this.width = 0;
        this.height = 0;
    }
}

When I run tsc BoxSample.ts, TypeScript reports a duplicate constructor definition. How should constructor overloading be written correctly in TypeScript?

Short Answer

By the end of this page, you will understand how constructor overloading works in TypeScript, why multiple constructor bodies are not allowed, and how to define overload signatures with a single implementation. You will also see practical examples, common mistakes, and a small project to reinforce the pattern.

Concept

In TypeScript, constructors can be overloaded, but not in the same way as in some other languages such as Java or C#.

The key rule is:

  • You may write multiple constructor signatures
  • But you must provide exactly one constructor implementation

That means this is allowed in TypeScript:

class Example {
    constructor();
    constructor(name: string);
    constructor(name?: string) {
        // one real constructor body
    }
}

But this is not allowed:

class Example {
    constructor() {}
    constructor(name: string) {}
}

TypeScript only lets a class have one actual runtime constructor because JavaScript itself only has one constructor function per class.

Why this matters

TypeScript adds type information on top of JavaScript. Overloads are mainly a type-checking feature. They help describe different valid ways to call the same constructor, while still compiling down to one JavaScript constructor.

This matters in real programming because classes often need flexible creation patterns, such as:

  • creating an object with no arguments
  • creating it from a configuration object
  • creating it from separate values

Constructor overloads let you support these cases cleanly while keeping strong type safety.

Mental Model

Think of a TypeScript constructor like a single front door to a building.

You can put several signs outside the door saying:

  • "You may enter with no arguments"
  • "You may enter with a box object"
  • "You may enter with x, y, width, and height"

Those signs are the overload signatures.

But inside the building, there is still only one actual doorway. That doorway is the single constructor implementation.

So overloads describe the allowed ways to arrive, but one constructor body handles all the cases.

Take Quiz

Syntax and Examples

Basic syntax

In TypeScript, constructor overloading looks like this:

class Box {
    x: number;
    y: number;
    width: number;
    height: number;

    constructor();
    constructor(obj: IBox);
    constructor(obj?: IBox) {
        if (obj) {
            this.x = obj.x;
            this.y = obj.y;
            this.width = obj.width;
            this.height = obj.height;
        } else {
            this.x = 0;
            this.y = 0;
            this.width = 0;
            this.height = 0;
        }
    }
}

Example using your IBox interface

interface IBox {
    x: number;
    y: number;
    height: number;
    width: number;
}

class Box {
    public x: number;
    public y: number;
    public height: number;
    public width: number;

    constructor();
    constructor(obj: IBox);
    constructor(obj?: IBox) {
        if (obj) {
            this.x = obj.x;
            this.y = obj.y;
            this.height = obj.height;
            this.width = obj.width;
        } else {
            this.x = 0;
            this.y = 0;
            this.width = 0;
            this.height = 0;
        }
    }
}

const a = new Box();
const b = new Box({ x: 10, y: 20, width: 30, height: 40 });

What this does

  • constructor(); declares that new Box() is valid
  • constructor(obj: IBox); declares that new Box(someBox) is valid
  • constructor(obj?: IBox) is the one implementation that handles both cases

Another example with multiple parameter shapes

class Point {
    x: number;
    y: number;

    constructor();
    constructor(x: number, y: number);
    constructor(x?: number, y?: number) {
        this.x = x ?? 0;
        this.y = y ?? 0;
    }
}

const p1 = new Point();
const p2 = new Point(5, 8);

The implementation uses optional parameters because it must be flexible enough to support all declared overloads.

Step by Step Execution

Consider this code:

interface IBox {
    x: number;
    y: number;
    width: number;
    height: number;
}

class Box {
    x: number;
    y: number;
    width: number;
    height: number;

    constructor();
    constructor(obj: IBox);
    constructor(obj?: IBox) {
        if (obj) {
            this.x = obj.x;
            this.y = obj.y;
            this.width = obj.width;
            this.height = obj.height;
        } else {
            this.x = 0;
            this.y = 0;
            this.width = 0;
            this.height = 0;
        }
    }
}

const box1 = new Box();
const box2 = new Box({ x: 1, y: 2, width: 3, height: 4 });

What happens with new Box()

  1. TypeScript checks the overload signatures.
  2. It sees that constructor(); matches.
  3. The actual implementation runs with obj as undefined.
  4. The if (obj) check is false.
  5. Default values are assigned:
    • x = 0
    • y = 0
    • width = 0
    • height = 0

What happens with new Box({ x: 1, y: 2, width: 3, height: 4 })

  1. TypeScript checks the overload signatures.
  2. It sees that constructor(obj: IBox); matches.
  3. The implementation runs with obj holding the provided object.
  4. The if (obj) check is true.
  5. Values are copied from the object into the class properties.

Important detail

The overload signatures are only used by TypeScript during type checking. At runtime, JavaScript only sees one constructor function.

Real World Use Cases

Constructor overloads are useful when a class can be created in more than one valid way.

Common scenarios

Default or configured object creation

const defaultBox = new Box();
const configuredBox = new Box({ x: 5, y: 5, width: 100, height: 50 });

Useful for UI components, game objects, and drawing libraries.

Building domain models

A class may allow either:

  • no arguments for safe defaults
  • an object argument for existing data

This is common in API response models, settings objects, and DTO-style classes.

Wrapper classes

A class may support initialization from different input formats.

class User {
    id: number;
    name: string;

    constructor();
    constructor(data: { id: number; name: string });
    constructor(data?: { id: number; name: string }) {
        this.id = data?.id ?? 0;
        this.name = data?.name ?? "";
    }
}

Libraries and reusable components

Overloads help make APIs easier to use because callers can choose the form that fits their situation while still getting autocomplete and type checking.

Take Quiz

Real Codebase Usage

In real projects, developers often use constructor overloads together with a few common patterns.

1. Default values

A constructor often accepts optional input and fills in missing values.

class Config {
    host: string;
    port: number;

    constructor();
    constructor(data: Partial<Config>);
    constructor(data?: Partial<Config>) {
        this.host = data?.host ?? "localhost";
        this.port = data?.port ?? 3000;
    }
}

2. Validation inside the constructor

class User {
    name: string;

    constructor(name: string);
    constructor(data: { name: string });
    constructor(value: string | { name: string }) {
        const name = typeof value === "string" ? value : value.name;

        if (!name) {
            throw new Error("Name is required");
        }

        this.name = name;
    }
}

3. Guard clauses

Developers often branch early to keep constructor logic simple.

class Box {
    x: number;
    y: number;

    constructor();
    constructor(data: { x: number; y: number });
    constructor(data?: { x: number; y: number }) {
        if (!data) {
            this.x = 0;
            this.y = 0;
            return;
        }

        this.x = data.x;
        this.y = data.y;
    }
}

4. Prefer object parameters when many values are needed

When constructors take many arguments, teams often switch to one object parameter because it is easier to read and maintain.

new Box({ x: 10, y: 20, width: 30, height: 40 });

This avoids mistakes caused by argument order.

Common Mistakes

1. Writing multiple constructor bodies

This is the original problem.

Incorrect

class Box {
    constructor() {}
    constructor(obj: IBox) {}
}

Why it fails

TypeScript allows multiple signatures, but only one implementation.

Correct

class Box {
    constructor();
    constructor(obj: IBox);
    constructor(obj?: IBox) {}
}

2. Making the implementation narrower than the overloads

The implementation must be able to handle every overload.

Incorrect

class Box {
    constructor();
    constructor(obj: IBox);
    constructor(obj: IBox) {
        // fails because new Box() would not fit this implementation
    }
}

Correct

class Box {
    constructor();
    constructor(obj: IBox);
    constructor(obj?: IBox) {
        // handles both cases
    }
}

3. Forgetting runtime checks

Overload signatures do not automatically separate behavior at runtime.

Incorrect

class Box {
    constructor();
    constructor(obj: IBox);
    constructor(obj?: IBox) {
        this.x = obj.x; // error: obj may be undefined
    }
}

Correct

class Box {
    x: number = 0;

    constructor();
    constructor(obj: IBox);
    constructor(obj?: IBox) {
        if (obj) {
            this.x = obj.x;
        }
    }
}

4. Exposing overloads that the implementation does not really support

If you declare an overload, your constructor logic should actually handle it correctly.

5. Using too many positional parameters

This is valid, but harder to read:

new Box(10, 20, 30, 40);

For many values, an object is usually clearer:

new Box({ x: 10, y: 20, width: 30, height: 40 });

Comparisons

ConceptWhat it meansGood forLimitation
Constructor overloadsMultiple allowed call signatures, one implementationFlexible class creationNeeds runtime checks inside one constructor
Optional parametersA parameter may be omittedSimple variationsLess explicit when argument shapes differ a lot
Union typesA parameter may be one of several typesSupporting different input formsRequires type narrowing
Static factory methodsNamed creation methods like Box.fromObject()Clear intent and readable APIsMore methods to maintain

Constructor overloads vs optional parameters

class A {
    constructor(x?: number) {}
}

This is enough when the only variation is whether one value is present.

Constructor overloads vs union types

class B {
    constructor(value: string | number) {}
}

A union type works well when there is one parameter with multiple possible types.

Constructor overloads vs static factory methods

class Box {
    constructor(public x: number, public y: number) {}

    static fromObject(obj: { x: number; y: number }) {
        return new Box(obj.x, obj.y);
    }
}

Factory methods are often clearer when creation logic is more descriptive or more complex.

Use overloads when you want one class constructor with a few well-defined call patterns. Use factory methods when the different creation paths need meaningful names.

Take Quiz

Cheat Sheet

class MyClass {
    constructor();
    constructor(value: string);
    constructor(value?: string) {
        // one implementation only
    }
}

Rules

  • You can declare multiple constructor signatures.
  • You can only write one constructor body.
  • The implementation must handle all overload cases.
  • Overloads are checked by TypeScript at compile time.
  • JavaScript runtime only gets one constructor.

Common pattern

constructor();
constructor(obj: SomeType);
constructor(obj?: SomeType) {
    if (obj) {
        // use obj
    } else {
        // defaults
    }
}

Useful tools

  • Optional parameters: arg?: Type
  • Union types: arg: A | B
  • Nullish coalescing: value ?? defaultValue
  • Type guards: typeof, in, custom checks

Good practice

  • Prefer object parameters when there are many values.
  • Keep overloads simple and predictable.
  • Validate input if bad values would break the object.

Edge case

This is invalid:

constructor() {}
constructor(x: number) {}

This is valid:

constructor();
constructor(x: number);
constructor(x?: number) {}

FAQ

Can TypeScript have multiple constructors like Java or C#?

Not with multiple bodies. TypeScript supports multiple constructor signatures, but only one implementation.

Why does TypeScript report a duplicate constructor definition?

Because your class contains more than one actual constructor body. JavaScript only supports one constructor implementation.

Do constructor overloads exist at runtime?

No. They are a compile-time TypeScript feature. At runtime, only one JavaScript constructor exists.

When should I use overloads instead of optional parameters?

Use overloads when you want to describe distinct valid call patterns. Use optional parameters when the variation is simple.

Can I overload a constructor with different parameter types?

Yes, as long as the single implementation can handle all cases.

constructor(value: string);
constructor(value: number);
constructor(value: string | number) {}

Is using an object parameter better than many arguments?

Often yes, especially when there are three or more related values. It improves readability and reduces argument-order mistakes.

Should I use a constructor overload or a static factory method?

Use overloads for a small number of simple creation patterns. Use factory methods when the creation paths need clearer names or more complex logic.

Take Quiz

Mini Project

Description

Build a small Rectangle class that can be created in two ways: with no arguments for a default rectangle, or with a configuration object containing position and size. This demonstrates how constructor overloads make class creation flexible while still using one implementation.

Goal

Create a Rectangle class that supports both default construction and object-based construction, and includes an area() method.

Requirements

  • Create an interface for rectangle data with x, y, width, and height
  • Add constructor overload signatures for no arguments and for one object argument
  • Implement one constructor that handles both cases safely
  • Add an area() method that returns width × height
  • Create and test at least two rectangle instances
Take Quiz

Keep learning

Related questions

View more