This TypeScript error occurs when you define multiple constructor implementations in a single class. Unlike some languages, TypeScript allows multiple constructor signatures for overloading but requires only one implementation that handles all cases.
The "Duplicate constructor definition" error (TS2392) appears when you attempt to define multiple constructor implementations in a TypeScript class. This is a fundamental difference from languages like Java or C# that support traditional constructor overloading with multiple separate implementations. TypeScript does support constructor overloading, but with a specific pattern: you can declare multiple constructor signatures (type declarations without bodies), but you must provide exactly one constructor implementation that handles all the overload cases. This design aligns with JavaScript's runtime behavior, where classes can only have one actual constructor function. TypeScript's type system adds the ability to describe different ways to call that constructor, but the underlying JavaScript still needs a single implementation. The error typically occurs when developers familiar with other languages try to write multiple constructor implementations, or when accidentally duplicating a constructor during refactoring.
First, locate all constructor definitions in your class. Look for multiple blocks with implementation bodies:
// WRONG - Multiple constructor implementations
class Person {
name: string;
age: number;
constructor() {
this.name = "Unknown";
this.age = 0;
}
constructor(name: string) {
this.name = name;
this.age = 0;
}
}
// Error: Duplicate constructor definitionRemove all but one constructor, keeping the one with the most complete logic, or the one you plan to expand into a single unified implementation.
Use TypeScript's constructor overload pattern: declare multiple signatures (without bodies) followed by a single implementation:
// CORRECT - Constructor overloads with single implementation
class Person {
name: string;
age: number;
// Overload signatures (no implementation bodies)
constructor();
constructor(name: string);
constructor(name: string, age: number);
// Single implementation that handles all overloads
constructor(name?: string, age?: number) {
this.name = name ?? "Unknown";
this.age = age ?? 0;
}
}
// All of these work:
const p1 = new Person(); // Uses default values
const p2 = new Person("Alice"); // name only
const p3 = new Person("Bob", 30); // name and ageThe implementation signature must be compatible with all overload signatures. Use optional parameters (?) or union types (string | number) to accept all variations.
When overloads accept different types, use type guards to determine which overload was called:
class Point {
x: number;
y: number;
// Overload: accept two numbers
constructor(x: number, y: number);
// Overload: accept a string like "10,20"
constructor(coords: string);
// Implementation handles both
constructor(xOrCoords: number | string, y?: number) {
if (typeof xOrCoords === "string") {
// Parse string format
const [x, y] = xOrCoords.split(",").map(Number);
this.x = x;
this.y = y;
} else {
// Use number parameters
this.x = xOrCoords;
this.y = y!;
}
}
}
const p1 = new Point(10, 20); // Using numbers
const p2 = new Point("10,20"); // Using stringThe implementation uses typeof checks or other type guards to branch based on which overload signature was matched.
For complex initialization logic, consider static factory methods instead of constructor overloading:
class User {
private name: string;
private email: string;
// Single private constructor
private constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
// Static factory methods for different creation patterns
static fromCredentials(username: string, password: string): User {
// Authenticate and create user
const email = `${username}@example.com`;
return new User(username, email);
}
static fromToken(token: string): User {
// Parse JWT or session token
const { name, email } = parseToken(token);
return new User(name, email);
}
static guest(): User {
return new User("Guest", "[email protected]");
}
}
// Clear, explicit creation methods
const user1 = User.fromCredentials("alice", "pass123");
const user2 = User.fromToken("eyJhbGc...");
const user3 = User.guest();This pattern provides clearer intent and allows different initialization logic without complex overload signatures.
If you have multiple constructors with different logic, combine them into one implementation:
// Before (WRONG):
class Product {
id: string;
price: number;
constructor() {
this.id = generateId();
this.price = 0;
}
constructor(price: number) {
this.id = generateId();
this.price = price;
}
}
// After (CORRECT):
class Product {
id: string;
price: number;
constructor(price: number = 0) {
this.id = generateId();
this.price = price;
}
}
const p1 = new Product(); // Uses default price 0
const p2 = new Product(99); // Sets price to 99Use default parameter values, optional parameters, or conditional logic to handle different initialization scenarios.
### Why TypeScript Differs from Java/C#
In Java or C#, you can write multiple constructors with different bodies:
// Java - this is valid
class Person {
String name;
int age;
Person() {
this.name = "Unknown";
this.age = 0;
}
Person(String name) {
this.name = name;
this.age = 0;
}
Person(String name, int age) {
this.name = name;
this.age = age;
}
}TypeScript can't do this because it compiles to JavaScript, which only supports a single constructor per class. TypeScript's overload signatures are purely compile-time type checks—they don't generate multiple runtime constructors.
### Constructor Overload Signature Compatibility
The implementation signature must be compatible with all overload signatures. This means:
class Example {
// Overload signatures
constructor(x: string);
constructor(x: number);
// Implementation must accept both string and number
constructor(x: string | number) {
// Implementation
}
}If your implementation signature is too narrow, you'll get an error:
class Example {
constructor(x: string);
constructor(x: number);
constructor(x: string) {
// Error: This overload signature is not compatible with its implementation signature
}
}The implementation signature doesn't need to be an overload itself—it's automatically considered the implementation if it has a body.
### Using Options Objects Instead
For classes with many optional parameters, consider using an options object instead of multiple overloads:
interface UserOptions {
name?: string;
email?: string;
age?: number;
role?: "admin" | "user";
}
class User {
name: string;
email: string;
age: number;
role: "admin" | "user";
constructor(options: UserOptions = {}) {
this.name = options.name ?? "Unknown";
this.email = options.email ?? "";
this.age = options.age ?? 0;
this.role = options.role ?? "user";
}
}
// Clean, self-documenting usage
const user = new User({
name: "Alice",
role: "admin",
age: 30,
});This approach scales better than overloads when you have many optional configurations.
### Abstract Classes and Constructors
Abstract classes can have constructor overloads, but the same rules apply:
abstract class Animal {
name: string;
constructor();
constructor(name: string);
constructor(name?: string) {
this.name = name ?? "Unknown";
}
abstract makeSound(): void;
}
class Dog extends Animal {
constructor(name: string) {
super(name); // Calls parent constructor
}
makeSound() {
console.log("Woof!");
}
}Derived classes don't inherit constructor overloads—they must call super() with arguments compatible with the parent's implementation signature.
### Generic Class Constructors
Generic classes can use overloads, but you can't overload based on type parameters:
// WRONG - Can't overload on generic type
class Box<T> {
constructor(value: T); // when T is string
constructor(value: T); // when T is number - This is a duplicate!
}
// CORRECT - Single constructor with generic type
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
const stringBox = new Box<string>("hello");
const numberBox = new Box<number>(42);Type parameters are erased at runtime, so overloading on them is impossible.
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript