Constructor Overloading
Multiple Signatures and the Factory Method Pattern
LinkedIn Hook
Java lets you write three constructors for the same class. C# lets you write five. Python lets you fake it with default parameters. JavaScript gives you exactly one constructor per class — full stop.
Most developers hit that wall, shrug, and stuff a pile of
ifchecks into their single constructor. It works. It also becomes unreadable by the third conditional.There's a cleaner way. Static factory methods — also called named constructors — give you all the flexibility of constructor overloading without bending the language against itself. They're explicit, self-documenting, and they let your constructor stay focused on one thing.
TypeScript adds constructor overloads on top of this, giving you type safety at the signature level while still running a single implementation underneath.
In Lesson 5.4, you'll see the JavaScript limitation clearly, understand why factory methods are the idiomatic solution, and learn how TypeScript's overload syntax works at both the type level and the runtime level.
Read the full lesson -> [link]
#OOP #JavaScript #TypeScript #InterviewPrep #DesignPatterns
What You'll Learn
- Why JavaScript only allows one constructor per class and what happens when you try to work around it
- How to simulate multiple constructor signatures using conditional logic inside a single constructor
- How static factory methods (named constructors) replace overloading with a cleaner, more readable pattern
- How to combine factory methods with private constructors for stricter API control
- How TypeScript constructor overload signatures work and what they compile down to
The Analogy — One Door, Multiple Keys
Imagine a building with a single entrance. Every visitor must use that one door. In most buildings, that's fine. But some buildings need to handle very different visitors: contractors who arrive with a full set of blueprints, guests who just need a room number, and maintenance crews who enter with a master key. Each visitor type needs a different check-in process.
You have two options. Option one: put a single receptionist at the door and have them ask twenty questions to figure out who they're dealing with. It works, but it slows everything down and the questions pile up. Option two: set up three dedicated check-in counters, each clearly labeled for one visitor type. Each counter handles its own flow, and the main door stays clean.
Static factory methods are the dedicated check-in counters. The single constructor is the one door that all of them eventually lead through. The building still has one entrance — but visitors no longer have to explain themselves at the door.
Why JavaScript Has Only One Constructor
JavaScript classes allow exactly one constructor method per class definition. This is not a design oversight. It reflects how JavaScript's prototype system works underneath the class syntax. When you write new MyClass(args), the engine calls one function: the constructor. There is no dispatch mechanism to choose between multiple constructor functions based on argument types or count.
In Java or C#, the compiler sees new Point(1, 2) and new Point("1,2") as two different calls and routes them to two different method signatures at compile time. JavaScript has no compiler in that sense and no type information at runtime to make that dispatch. Every new MyClass() call hits the same function.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
// Attempting to add a second constructor is a SyntaxError
// constructor(str) { // SyntaxError: A class may only have one constructor
// const [x, y] = str.split(',').map(Number);
// this.x = x;
// this.y = y;
// }
}
The Naive Fix — Conditional Logic Inside One Constructor
The first instinct many developers have is to put all the branching logic inside the single constructor. This works. It also scales poorly and blurs the intent of each initialization path.
class Color {
constructor(rOrHexOrObj, g, b) {
// Path 1: called with a hex string like '#ff6b35'
if (typeof rOrHexOrObj === 'string') {
const hex = rOrHexOrObj.replace('#', '');
this.r = parseInt(hex.slice(0, 2), 16);
this.g = parseInt(hex.slice(2, 4), 16);
this.b = parseInt(hex.slice(4, 6), 16);
}
// Path 2: called with an object like { r: 255, g: 107, b: 53 }
else if (typeof rOrHexOrObj === 'object' && rOrHexOrObj !== null) {
this.r = rOrHexOrObj.r;
this.g = rOrHexOrObj.g;
this.b = rOrHexOrObj.b;
}
// Path 3: called with three separate numbers like (255, 107, 53)
else {
this.r = rOrHexOrObj;
this.g = g;
this.b = b;
}
}
toString() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
const a = new Color('#ff6b35');
const b = new Color({ r: 255, g: 107, b: 53 });
const c = new Color(255, 107, 53);
console.log(a.toString()); // rgb(255, 107, 53)
console.log(b.toString()); // rgb(255, 107, 53)
console.log(c.toString()); // rgb(255, 107, 53)
This works, but the first parameter is named rOrHexOrObj — a smell that signals overloaded intent. The constructor is doing three different jobs. A reader cannot tell which initialization path they should use without reading the whole body. Every new initialization variant adds another branch.
[PERSONAL EXPERIENCE]: In production codebases, this pattern tends to grow. A two-branch constructor becomes a four-branch constructor over six months as new requirements arrive. By the time the fourth branch lands, nobody remembers what the first parameter was supposed to mean. Static factory methods stop this before it starts.
The Clean Fix — Static Factory Methods
A static factory method is a static method on the class that constructs and returns an instance. Each factory method has a clear, descriptive name. The real constructor can be kept minimal, doing only what every initialization path has in common. The factory methods handle the path-specific logic and then delegate to the constructor.
[UNIQUE INSIGHT]: Static factory methods are more than a workaround for the one-constructor limit. They let you name the intent of each creation path. Color.fromHex('#ff6b35') is self-documenting in a way that new Color('#ff6b35') is not. The name carries information that argument type alone cannot carry.
class Color {
// The constructor stays minimal and focused
// It only accepts the canonical form: three separate numbers
constructor(r, g, b) {
this.r = r;
this.g = g;
this.b = b;
}
// Named constructor 1: create from a hex string
static fromHex(hex) {
const clean = hex.replace('#', '');
const r = parseInt(clean.slice(0, 2), 16);
const g = parseInt(clean.slice(2, 4), 16);
const b = parseInt(clean.slice(4, 6), 16);
return new Color(r, g, b);
}
// Named constructor 2: create from an object with r, g, b keys
static fromObject({ r, g, b }) {
return new Color(r, g, b);
}
// Named constructor 3: create a default gray color
static gray(brightness = 128) {
return new Color(brightness, brightness, brightness);
}
// Named constructor 4: create from a CSS rgb string like 'rgb(255, 107, 53)'
static fromCSSString(css) {
const match = css.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (!match) {
throw new Error(`Invalid CSS rgb string: "${css}"`);
}
return new Color(Number(match[1]), Number(match[2]), Number(match[3]));
}
toString() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
toHex() {
const hex = (n) => n.toString(16).padStart(2, '0');
return `#${hex(this.r)}${hex(this.g)}${hex(this.b)}`;
}
}
const fromHex = Color.fromHex('#ff6b35');
const fromObj = Color.fromObject({ r: 255, g: 107, b: 53 });
const defaultGray = Color.gray();
const fromCSS = Color.fromCSSString('rgb(255, 107, 53)');
console.log(fromHex.toString()); // rgb(255, 107, 53)
console.log(fromObj.toHex()); // #ff6b35
console.log(defaultGray.toString()); // rgb(128, 128, 128)
console.log(fromCSS.toHex()); // #ff6b35
Each factory method has one job. The constructor has one job. Adding a new creation path means adding one new static method, not modifying an existing conditional chain.
[INTERNAL-LINK: static methods in JavaScript -> article on static vs instance methods in OOP]
Restricting Construction — Private-Style Constructors
In some designs, you want to force callers to use the factory methods exclusively. Direct new ClassName() calls should not be allowed. JavaScript has no private constructor keyword, but you can get close using a sentinel value check or the private class fields proposal.
// Sentinel-based approach: pass a private Symbol to the constructor
// Only the class itself holds the Symbol — outside callers cannot forge it
const _token = Symbol('Color.constructorToken');
class StrictColor {
constructor(token, r, g, b) {
// Reject any call that doesn't carry the internal token
if (token !== _token) {
throw new Error(
'StrictColor cannot be instantiated directly. ' +
'Use StrictColor.fromHex(), StrictColor.fromRGB(), etc.'
);
}
this.r = r;
this.g = g;
this.b = b;
}
static fromHex(hex) {
const clean = hex.replace('#', '');
const r = parseInt(clean.slice(0, 2), 16);
const g = parseInt(clean.slice(2, 4), 16);
const b = parseInt(clean.slice(4, 6), 16);
// Only the factory methods can call new StrictColor() because only they
// have access to _token (it's defined in the same module scope)
return new StrictColor(_token, r, g, b);
}
static fromRGB(r, g, b) {
return new StrictColor(_token, r, g, b);
}
toString() {
return `rgb(${this.r}, ${this.g}, ${this.b})`;
}
}
// Factory methods work correctly
const color = StrictColor.fromHex('#00ff87');
console.log(color.toString()); // rgb(0, 255, 135)
// Direct instantiation is blocked
try {
const bad = new StrictColor(null, 0, 255, 135);
} catch (e) {
console.log(e.message);
// StrictColor cannot be instantiated directly. Use StrictColor.fromHex(), ...
}
The Symbol _token is defined in module scope. Outside callers have no reference to it and cannot pass the correct value. This is a soft private constructor — it's a convention enforced by module boundaries rather than a language keyword.
[INTERNAL-LINK: Symbol usage in JavaScript -> article on Symbols and well-known symbols]
TypeScript Constructor Overloads
TypeScript supports constructor overloads. You declare multiple constructor signatures at the type level, then write a single implementation signature that is compatible with all of them. The overload signatures are compile-time only. The implementation signature is the one that actually runs.
class Point {
x: number;
y: number;
// Overload signature 1: create from two numbers
constructor(x: number, y: number);
// Overload signature 2: create from a string like "3,4"
constructor(coords: string);
// Overload signature 3: create at the origin
constructor();
// Implementation signature: must be compatible with ALL overloads above
// TypeScript will NOT expose this signature to callers — only the overloads above are visible
constructor(xOrCoords?: number | string, y?: number) {
if (typeof xOrCoords === 'string') {
// Handle "3,4" string input
const parts = xOrCoords.split(',').map(Number);
this.x = parts[0];
this.y = parts[1];
} else if (typeof xOrCoords === 'number' && typeof y === 'number') {
// Handle two separate numbers
this.x = xOrCoords;
this.y = y;
} else {
// Handle no-argument call — default to origin
this.x = 0;
this.y = 0;
}
}
distanceTo(other: Point): number {
const dx = this.x - other.x;
const dy = this.y - other.y;
return Math.sqrt(dx * dx + dy * dy);
}
toString(): string {
return `Point(${this.x}, ${this.y})`;
}
}
const a = new Point(3, 4);
const b = new Point('6,8');
const origin = new Point();
console.log(a.toString()); // Point(3, 4)
console.log(b.toString()); // Point(6, 8)
console.log(origin.toString()); // Point(0, 0)
console.log(a.distanceTo(origin)); // 5
// TypeScript compiler rejects invalid call patterns:
// new Point(true); // Error: No overload matches this call.
// new Point(1, 2, 3); // Error: No overload matches this call.
// new Point({ x: 1 }); // Error: No overload matches this call.
The overload signatures are purely type-level declarations. When TypeScript compiles this to JavaScript, the output is a single constructor function with the union types collapsed. The overload lines vanish entirely.
[ORIGINAL DATA]: TypeScript constructor overloads follow the same rules as regular function overloads in TypeScript: the implementation signature must be the most general (widest) signature. TypeScript itself uses this pattern in its standard library. The Date constructor, for example, has four overload signatures in lib.es5.d.ts — no arguments, a number, a string, or individual year/month/day components. All four dispatch to one implementation.
[INTERNAL-LINK: TypeScript overload signatures -> article on TypeScript function overloads]
Combining TypeScript Overloads With Static Factories
TypeScript overloads and static factory methods are not mutually exclusive. For complex initialization scenarios, you can use both. The overloads provide type safety for the most common call patterns. The factory methods handle specialized or optional logic while keeping each path clearly named.
class Connection {
readonly host: string;
readonly port: number;
readonly protocol: 'http' | 'https';
// Overloaded constructor for the two most common creation patterns
constructor(url: string);
constructor(host: string, port: number, protocol?: 'http' | 'https');
// Implementation — handles both overloads
constructor(
hostOrUrl: string,
port?: number,
protocol: 'http' | 'https' = 'https'
) {
if (port === undefined) {
// Treat the first argument as a full URL
const parsed = new URL(hostOrUrl);
this.host = parsed.hostname;
this.port = parsed.port ? Number(parsed.port) : 443;
this.protocol = parsed.protocol.replace(':', '') as 'http' | 'https';
} else {
this.host = hostOrUrl;
this.port = port;
this.protocol = protocol;
}
}
// Static factory for a local development connection
// More descriptive than new Connection('localhost', 3000, 'http')
static localhost(port: number = 3000): Connection {
return new Connection('localhost', port, 'http');
}
// Static factory for a production connection
static production(host: string): Connection {
return new Connection(host, 443, 'https');
}
toString(): string {
return `${this.protocol}://${this.host}:${this.port}`;
}
}
const fromURL = new Connection('https://api.example.com:8443');
const fromParts = new Connection('api.example.com', 8443, 'https');
const dev = Connection.localhost(4000);
const prod = Connection.production('api.example.com');
console.log(fromURL.toString()); // https://api.example.com:8443
console.log(fromParts.toString()); // https://api.example.com:8443
console.log(dev.toString()); // http://localhost:4000
console.log(prod.toString()); // https://api.example.com:443
The overloads handle the two common construction patterns with full type safety. The factory methods name the intent for the two common use-case contexts. Neither pattern is redundant — they operate at different levels of abstraction.
Common Mistakes
-
Writing multiple constructor blocks in JavaScript. JavaScript throws a
SyntaxErrorif you define more than oneconstructorin a class. This is not a lint warning or a runtime error that you can catch. The parser rejects it before execution. If you're coming from Java or C#, this trips you up immediately. -
Making the implementation constructor in TypeScript a public overload. The implementation signature must not be listed in the public-facing overloads. It should be the last
constructor(...)declaration and its parameter types should be the union of all the overload signatures. If you accidentally expose the implementation signature, callers can pass raw union types directly, which defeats the purpose of the overloads. -
Not validating input in factory methods. A factory method should throw with a clear message when given invalid input. If
Color.fromHexsilently produces aColor(NaN, NaN, NaN)for an invalid hex string, debugging becomes painful. Validate at the factory boundary, not inside the constructor. -
Forgetting that factory methods are not constructors. Factory methods can return cached instances, singletons, or instances of subclasses. They can return
null. A constructor always returns a new instance of the declaring class. Reviewers from Java backgrounds sometimes treatstaticfactory methods as if they have the same constraints as constructors. They do not. -
Overloading the overloads. TypeScript constructor overloads work best when there are 2-4 distinct signatures. More than that and the implementation signature becomes a deeply nested conditional. At that point, static factory methods are the cleaner solution even in TypeScript, because each factory method is independently testable and readable.
Interview Questions
Q: Why can't you have multiple constructors in JavaScript?
JavaScript classes allow exactly one
constructormethod. This is because there is no compile-time type dispatch. When you callnew MyClass(args), the JavaScript engine calls one function. It has no mechanism to choose between multiple constructor implementations based on argument types or count at runtime. Attempting to define a secondconstructorblock produces aSyntaxErrorat parse time.
Q: What is a static factory method, and how does it replace constructor overloading in JavaScript?
A static factory method is a
staticmethod on the class that constructs and returns an instance. Instead of branching inside a single constructor, each creation path gets its own named static method. The constructor handles only the common initialization logic. Factory methods likeColor.fromHex()orConnection.localhost()make the intent of each creation path explicit, which a conditional inside a single constructor cannot do.
Q: What is the difference between a TypeScript constructor overload signature and the implementation signature?
The overload signatures are type declarations only. They define the valid argument patterns that TypeScript will accept at call sites. The implementation signature is the one that actually runs — it must be compatible with all overload signatures, which typically means union types and optional parameters. Callers see only the overload signatures. The implementation signature is hidden from the public API. When compiled to JavaScript, the overload lines are erased entirely.
Q: How would you simulate a private constructor in JavaScript?
JavaScript has no
private constructorkeyword. A common approach is to define a module-scopedSymboland require it as the first argument to the constructor. If the caller doesn't pass the correct Symbol, the constructor throws. Because the Symbol is defined in the module's private scope, external code has no reference to it and cannot forge a valid call. Only the class's own static factory methods, which share the same scope, can construct instances.
Q: When would you choose static factory methods over TypeScript constructor overloads?
Static factory methods are preferable when each creation path has significantly different logic, when you want to cache or reuse instances, when you need the method to be independently named and testable, or when more than three or four different initialization patterns exist. TypeScript constructor overloads are preferable when the differences are primarily type-level (same logical flow, different input shapes) and when compile-time type checking is the main goal. The two approaches can also be combined.
Quick Reference — Cheat Sheet
CONSTRUCTOR OVERLOADING IN JS / TS
====================================
JavaScript Rule
----------------
One constructor() per class — enforced by the parser (SyntaxError if violated)
No compile-time type dispatch — runtime only
Solution: static factory methods (named constructors)
Pattern 1: Conditional Inside Single Constructor
--------------------------------------------------
class Point {
constructor(xOrStr, y) {
if (typeof xOrStr === 'string') { ... }
else { this.x = xOrStr; this.y = y; }
}
}
+ Works, requires no extra syntax
- Grows unreadable with 3+ paths
- First parameter often gets an ugly name (xOrStrOrObj)
Pattern 2: Static Factory Methods (Preferred)
-----------------------------------------------
class Point {
constructor(x, y) { this.x = x; this.y = y; }
static fromString(str) {
const [x, y] = str.split(',').map(Number);
return new Point(x, y);
}
static origin() {
return new Point(0, 0);
}
}
+ Each path is named and independently readable
+ Constructor stays focused
+ Factory methods can validate, cache, or return subclasses
+ Easy to test each path in isolation
- Callers must know the factory method names
Pattern 3: Private-Style Constructor (Sentinel Symbol)
--------------------------------------------------------
const _token = Symbol('ClassName.token');
class StrictPoint {
constructor(token, x, y) {
if (token !== _token) throw new Error('Use StrictPoint.from*() factories');
this.x = x; this.y = y;
}
static fromString(str) {
const [x, y] = str.split(',').map(Number);
return new StrictPoint(_token, x, y);
}
}
+ Forces all callers through factory methods
+ Symbol cannot be forged from outside the module
- Slightly awkward constructor signature
Pattern 4: TypeScript Constructor Overloads
--------------------------------------------
class Point {
constructor(x: number, y: number); // Overload 1 (type only)
constructor(coords: string); // Overload 2 (type only)
constructor(xOrCoords?: number | string, y?: number) { // Implementation
// Runtime branching here
}
}
+ Compile-time type safety on call sites
+ No extra static methods needed for simple cases
+ IDE autocomplete shows the named overloads
- Implementation signature grows complex with many overloads
- Overload declarations are erased in compiled JS
KEY RULES
----------
Static factory methods > conditional constructor (readability and maintainability)
TypeScript overloads = compile-time only (erased by tsc)
Sentinel Symbol = closest to private constructor in JS
Factory methods can return cached instances; constructors cannot
WHEN TO USE EACH
-----------------
2-4 simple type-based variants -> TypeScript overloads
Different logic per creation path -> Static factory methods
Force factory-only usage -> Sentinel Symbol + factories
Complex creation with caching -> Static factory methods only
Previous: Lesson 5.3 - Method Overloading Next: Lesson 5.5 - Overloading vs Overriding
This is Lesson 5.4 of the OOP Interview Prep Course — 8 chapters, 41 lessons.