In TypeScript, what does marking a class constructor `private` do, and how does a `protected constructor` differ?
answer
- construction moves behind a static method
- two things get blocked, not one
- super() is a call to the base constructor
- the middle tier keeps inheritance open
- singletons, parse-or-null, async connect
basics
~20 sA private constructor stops outside code from writing new C() and also stops the class from being extended, leaving static factory methods inside the class as the only way to build instances. A protected constructor blocks direct new but still permits subclasses.
solid answer
~40 sMarking the constructor `private` means only code inside the class body may call it, so `new C()` from outside is a type error and `class Sub extends C {}` is rejected as well — a derived constructor would have to call `super()`, which it cannot see. That leaves a static method on the class as the sanctioned entry point, which is how singletons, cached instances, validating factories and `from…`-style named constructors are expressed. `protected constructor` relaxes exactly one of those rules: outside code still cannot call `new`, but subclasses may extend the class and call `super()`, which is the shape you want for a base class that must never be instantiated directly. Both checks are compile-time only; the emitted JavaScript has an ordinary constructor, so `new (C as any)()` works at run time.
code
typescript · 24 linesclass Config {
private static instance: Config | null = null;
private url: string;
private constructor(url: string) {
this.url = url;
}
static get(url: string): Config {
if (Config.instance === null) {
Config.instance = new Config(url); // legal: inside the class body
}
return Config.instance;
}
getUrl(): string {
return this.url;
}
}
const c = Config.get("https://example.com");
console.log(c.getUrl());
// new Config("x"); // error: constructor is private
// class Sub extends Config {} // error: cannot extend a private-constructor classgo deeper
Know that a private constructor means outside code cannot write new C(), and that a static method on the class becomes the way to obtain an instance. Recognising the singleton and factory shapes is enough here.
Explain why extends is blocked too - a derived constructor must call super(), which it cannot reach - and that protected is exactly the tier that reopens inheritance while keeping new closed. Name the factory cases a constructor cannot cover: failing without throwing, and async setup.
Show judgment about when closing construction is worth it: validating factories that return a result instead of throwing, caches, and objects needing async initialisation. Be clear that the modifier is erased, so uniqueness invariants need real enforcement in the constructor body.
Own the API-design consequence: a closed constructor removes an extension point for every downstream consumer, interacts badly with generic factories and DI containers that expect construct signatures, and is difficult to loosen later without a version bump. Decide when the ergonomics justify it.
## What the modifier does A constructor is a class member, so it takes the same visibility modifiers as any other member. Marking it `private` restricts *calls* to the body of the declaring class: ```ts class Config { private constructor(private readonly n: number) {} } ``` (That parameter shorthand aside, the point is the modifier.) From outside, `new Config(1)` is a compile error. From inside — including from a `static` method, which is inside the class body — it is perfectly legal. ## Why it also blocks `extends` This is the part candidates miss. A derived class constructor must call `super()`, and `super()` is a call to the base constructor. If that constructor is private, the subclass cannot make the call, so TypeScript rejects the inheritance outright rather than waiting for the `super()` line: it reports that the class cannot be extended because its constructor is marked private. `protected` is the modifier that keeps `new` closed to outsiders while leaving `super()` open to subclasses. | Modifier | `new C()` outside | `class D extends C` | | --- | --- | --- | | (public, default) | allowed | allowed | | `protected` | error | allowed | | `private` | error | error | ## What the pattern buys you Closing the constructor moves construction behind a named, static entry point you control. That enables: - **Singletons / caches.** A `static getInstance()` returns a stored instance instead of a fresh one. - **Validating factories.** A `static parse(raw: string): Result | null` can fail without throwing, which a constructor cannot do — a constructor must either return an instance or throw. - **Named constructors.** `Duration.fromSeconds` and `Duration.fromMinutes` read better than one overloaded constructor with a mode flag. - **Async construction.** A `static async connect()` can await work before handing back a fully initialised object; constructors cannot be async. ```ts class Duration { private ms: number; private constructor(ms: number) { this.ms = ms; } static fromSeconds(s: number) { return new Duration(s * 1000); } static fromMinutes(m: number) { return new Duration(m * 60_000); } } ``` ## The `protected constructor` shape Use `protected` when the class is a base that should never be constructed on its own but must be subclassed: ```ts class Handler { protected constructor(readonly name: string) {} } class JsonHandler extends Handler { constructor() { super("json"); } // ok } // new Handler("x"); // error ``` This is close to what an abstract class gives you, and the choice between the two is a real design question: a protected constructor still allows a fully concrete class with no members left unimplemented, whereas abstractness is about members the subclass must supply. ## The erasure caveat As with every visibility modifier, this is checked and then erased. The emitted JavaScript has a plain constructor; `new (Config as any)(1)` runs happily, and a JavaScript consumer of your compiled package can call it directly. So a private constructor is a design constraint expressed to TypeScript callers, not an invariant the runtime protects. If instances must be impossible to forge, the enforcement has to live in the constructor body or in a brand the factory alone can produce. ## Interaction with type positions One more consequence worth knowing: a class whose constructor is not public does not satisfy a construct signature such as `new () => T`, so it cannot be passed where a constructable value is expected — a generic factory helper that takes `ctor: new () => T` will reject it. That is usually a feature rather than a problem: you closed construction on purpose, so the class should not slip through a generic that reopens it.
- Why would you choose a private constructor over an abstract class for a base type?They solve different problems. `abstract` says some members are missing and the subclass must supply them; a `protected` constructor says the class is complete but must not be instantiated on its own. And a `private` constructor is not a base-class tool at all — it closes inheritance entirely, which abstractness never does.
- A generic helper takes `ctor: new () => T`. Why does a class with a private constructor fail to satisfy it?Because the construct signature promises that a caller can write `new ctor()`, and a non-public constructor makes that call illegal outside the class. The compiler therefore refuses the assignment. That is consistent: you deliberately closed construction, so a generic that reopens it should not accept the class.
- Does a private constructor guarantee that no other instance can ever exist at run time?No. The modifier is erased on emit, so `new (C as any)()` or a plain JavaScript caller constructs freely, and reflection over the compiled class sees an ordinary constructor. If uniqueness is a real invariant, enforce it in the constructor body or hand out only values the factory can brand.
saying these in an interview costs you the question
- Thinks a private constructor still allows subclassing
- Says protected constructors block extends as well
- Claims the class cannot be instantiated at all, even internally
- Believes the restriction is enforced at run time
- Confuses a private constructor with an abstract class