Loading
All articles
Programming 14 min read

TypeScript Protected Constructors: A Visual, Hands-On Guide

Learn TypeScript protected constructors visually: how to stop a base class from being built directly, the inherited constructor trap, what the check doesn't do at run time, private constructors, factories, singletons, abstract classes, how to choose, and exercises.

nbkhope
On this page

In TypeScript Class Inheritance you built a base class and its subclasses, and every one of them could be created with new. Sometimes that's wrong. A car is a real thing, and so is a motorcycle, but what would a plain "vehicle" be? You want the base class to exist so others can inherit from it, and you don't want anyone building it directly.

In this guide you'll solve that with a protected constructor, and meet the trap waiting in subclasses that don't write their own constructor. You'll see what the check doesn't do at run time, use private constructors for factories and singletons, compare both with abstract classes, and finish with a simple way to choose. Exercises wrap it up.

All outputs below come from real runs with TypeScript 7.0 and Node.js 26 on Windows. Save each example as main.ts, then run npx tsc main.ts and node main.js.

The problem

Here's a base class Vehicle and a derived class Car. The constructors are empty, so we can focus on who's allowed to call them:

1class Vehicle {
2  constructor() {}
3}
4
5class Car extends Vehicle {
6  constructor() {
7    super();
8  }
9}
10
11let myVehicle: Vehicle;
12myVehicle = new Car();
13console.log(myVehicle);
14
15const vehicle = new Vehicle();
16console.log(vehicle);
Car {}
Vehicle {}

The variable myVehicle has type Vehicle, and it can hold a Car, because a Car is a Vehicle. That's what we want. The last two lines are what we don't want: they build an object from the base class itself.

Adding protected

To forbid that, write protected before the base class's constructor:

1class Vehicle {
2  protected constructor() {}
3}
4
5class Car extends Vehicle {
6  constructor() {
7    super();
8  }
9}
10
11let myVehicle: Vehicle;
12myVehicle = new Car();
13console.log(myVehicle);
14
15const vehicle = new Vehicle();
main.ts(15,17): error TS2674: Constructor of class 'Vehicle' is protected and only accessible within the class declaration.

Now only the subclasses can be built. super() is still allowed, because Car is a subclass, and a variable can still have the type Vehicle.

A base class nobody builds directly. new Vehicle() is rejected with error TS2674, because its constructor is protected. Car extends Vehicle and has a public constructor, so new Car() works. Motorcycle extends Vehicle and has a public constructor, so new Motorcycle() works. Both call super() to reach the protected constructor

Subclasses usually pass something up. Here each kind of vehicle knows its own number of wheels, and the base class stores it:

1class Vehicle {
2  wheels: number;
3
4  protected constructor(wheels: number) {
5    this.wheels = wheels;
6  }
7}
8
9class Car extends Vehicle {
10  constructor() {
11    super(4);
12  }
13}
14
15class Motorcycle extends Vehicle {
16  constructor() {
17    super(2);
18  }
19}
20
21const vehicles: Vehicle[] = [new Car(), new Motorcycle()];
22console.log(vehicles);
[ Car { wheels: 4 }, Motorcycle { wheels: 2 } ]

The inherited constructor trap

Here's the mistake almost everyone makes. In the last guide, a subclass with nothing to add didn't need a constructor. Try that here:

1class Vehicle {
2  protected constructor() {}
3}
4
5class Car extends Vehicle {}
6
7const car = new Car();
8console.log(car);
main.ts(7,13): error TS2674: Constructor of class 'Vehicle' is protected and only accessible within the class declaration.
Car {}

The message names Vehicle, though the line says new Car(). With no constructor of its own, Car inherits the protected one. Each concrete subclass must declare a public constructor, even if all it does is call super.

That also gives you a middle level. A subclass can keep its constructor protected, so that it too can only be extended:

1class Vehicle {
2  protected constructor() {}
3}
4
5class Car extends Vehicle {
6  protected constructor() {
7    super();
8  }
9}
10
11class SportsCar extends Car {
12  constructor() {
13    super();
14  }
15}
16
17console.log(new SportsCar());
18new Car();
main.ts(18,1): error TS2674: Constructor of class 'Car' is protected and only accessible within the class declaration.
SportsCar {}

Building it from the inside

A protected constructor blocks code outside the class, and not the class itself. A static method can still call new, so the class can offer its own controlled way to build objects:

1class Vehicle {
2  wheels: number;
3
4  protected constructor(wheels: number) {
5    this.wheels = wheels;
6  }
7
8  static createBicycle(): Vehicle {
9    return new Vehicle(2);
10  }
11}
12
13console.log(Vehicle.createBicycle());
Vehicle { wheels: 2 }

So despite the wording of the error message, the rule is about who's calling: the class and its subclasses can, and everyone else can't.

It's only the compiler

Look at this program again, with the forbidden line:

1class Vehicle {
2  protected constructor() {}
3}
4
5const vehicle = new Vehicle();
6console.log(vehicle);
main.ts(5,17): error TS2674: Constructor of class 'Vehicle' is protected and only accessible within the class declaration.
Vehicle {}

tsc reported the error, wrote main.js anyway, and the program built the object. Like private, protected is removed from the JavaScript. The same happens with node main.ts, which printed Vehicle {} with no warning at all. Casting to any gets past the compiler too:

1class Vehicle {
2  protected constructor() {}
3}
4
5const vehicle = new (Vehicle as any)();
6console.log(vehicle);
Vehicle {}

If you need a real check when the program runs, new.target tells a constructor which class is being built:

1class Vehicle {
2  protected constructor() {
3    if (new.target === Vehicle) {
4      throw new Error("Vehicle is only a base class");
5    }
6  }
7}
8
9class Car extends Vehicle {
10  constructor() {
11    super();
12  }
13}
14
15console.log(new Car());
16
17try {
18  new (Vehicle as any)();
19} catch (error) {
20  console.log(String(error));
21}
Car {}
Error: Vehicle is only a base class

Most code doesn't need this. The compiler's check stops honest mistakes, as long as you act on the errors it reports.

Private constructors

private goes one step further: the constructor works only inside the class itself. That blocks new from outside, and it also blocks inheritance:

1class Vehicle {
2  private constructor() {}
3}
4
5class Car extends Vehicle {
6  constructor() {
7    super();
8  }
9}
10
11new Vehicle();
main.ts(5,19): error TS2675: Cannot extend a class 'Vehicle'. Class constructor is marked as private.
main.ts(11,1): error TS2673: Constructor of class 'Vehicle' is private and only accessible within the class declaration.

A class nobody can build sounds useless, but the class can still build itself. That gives you two common patterns.

The first is the factory method: named static methods that check their input. new Temperature(100) wouldn't say which scale you mean, and these names do:

1class Temperature {
2  readonly celsius: number;
3
4  private constructor(celsius: number) {
5    this.celsius = celsius;
6  }
7
8  static fromCelsius(celsius: number): Temperature {
9    if (celsius < -273.15) {
10      throw new Error(`Below absolute zero: ${celsius}`);
11    }
12    return new Temperature(celsius);
13  }
14
15  static fromFahrenheit(fahrenheit: number): Temperature {
16    return Temperature.fromCelsius((fahrenheit - 32) * 5 / 9);
17  }
18}
19
20console.log(Temperature.fromCelsius(20));
21console.log(Temperature.fromFahrenheit(212));
22
23try {
24  Temperature.fromCelsius(-300);
25} catch (error) {
26  console.log(String(error));
27}
Temperature { celsius: 20 }
Temperature { celsius: 100 }
Error: Below absolute zero: -300

Every Temperature in the program went through the check, because there's no other way to get one.

The second is the singleton: a class with exactly one instance, shared by everything that asks for it:

1class Settings {
2  private static instance?: Settings;
3  theme = "light";
4
5  private constructor() {}
6
7  static getInstance(): Settings {
8    if (Settings.instance === undefined) {
9      Settings.instance = new Settings();
10    }
11    return Settings.instance;
12  }
13}
14
15const first = Settings.getInstance();
16const second = Settings.getInstance();
17
18first.theme = "dark";
19
20console.log(second.theme);
21console.log(first === second);
dark
true

Changing first changed second, because they're the same object.

Abstract classes

TypeScript has a keyword made for "this is only a base class": abstract. It stops new just as a protected constructor does, and it does something extra: it can declare methods with no body, which every subclass must write:

1abstract class Vehicle {
2  abstract getWheels(): number;
3
4  describe(): string {
5    return `It has ${this.getWheels()} wheels`;
6  }
7}
8
9class Car extends Vehicle {
10  override getWheels(): number {
11    return 4;
12  }
13}
14
15class Motorcycle extends Vehicle {}
16
17console.log(new Car().describe());
18new Vehicle();
main.ts(15,7): error TS2515: Non-abstract class 'Motorcycle' does not implement inherited abstract member getWheels from class 'Vehicle'.
main.ts(18,1): error TS2511: Cannot create an instance of an abstract class.
It has 4 wheels

There are two advantages over a protected constructor. A subclass with no constructor can still be built, so the inherited constructor trap doesn't exist. And describe can call getWheels safely, because the compiler guarantees every subclass has one.

Who can build the object? With a public constructor, new from outside, super() in a subclass and new inside the class are all allowed. With a protected constructor, new from outside gives TS2674, and the other two are allowed. With a private constructor, new from outside gives TS2673, extending gives TS2675, and new inside the class is allowed. With an abstract class, new from outside gives TS2511, super() in a subclass is allowed, and new inside the class gives TS2511

Which one should you use?

Three questions settle it:

Choosing who can build a class. First, should anyone be able to call new? If so, use a public constructor. If not, should the class have subclasses? If not, use a private constructor with static methods. If it should, must subclasses write some of the methods? If so, use an abstract class. If not, a protected constructor is enough

In new code, abstract is usually the better way to say "only a base class". A protected constructor still has its place, when the class needs its own static methods to call new, which an abstract class can't do.

What constructors can't catch

A constructor's access controls one thing: calling new. It says nothing about the type of the objects. As you saw in the classes guide, types are compared by shape, so a plain object can pose as a Vehicle, protected constructor or not:

1class Vehicle {
2  wheels = 4;
3
4  protected constructor() {}
5}
6
7const imitation: Vehicle = { wheels: 4 };
8
9console.log(imitation);
10console.log(imitation instanceof Vehicle);
{ wheels: 4 }
false

This compiled with no errors. Nobody called the constructor, and the object still counts as a Vehicle. If you need to rule that out, a private or # property makes a class impossible to imitate this way.

The compiler does follow access when you pass the class around, though. new () => Vehicle is the type of "a class I can call new on", and a protected one doesn't fit:

1class Vehicle {
2  protected constructor() {}
3}
4
5class Car extends Vehicle {
6  constructor() {
7    super();
8  }
9}
10
11function build(VehicleClass: new () => Vehicle): Vehicle {
12  return new VehicleClass();
13}
14
15console.log(build(Car));
16console.log(build(Vehicle));
1main.ts(16,19): error TS2345: Argument of type 'typeof Vehicle' is not assignable to parameter of type 'new () => Vehicle'.
2  Cannot assign a 'protected' constructor type to a 'public' constructor type.
3Car {}
4Vehicle {}

Your turn: practice

Try these before peeking at the answers.

Exercise 1: Predict both tools (warm-up). What does tsc report for this program, and what does node main.js print afterward?

1class Shape {
2  protected constructor() {}
3}
4
5class Circle extends Shape {}
6
7class Square extends Shape {
8  constructor() {
9    super();
10  }
11}
12
13console.log(new Square());
14console.log(new Circle());
15console.log(new Shape());

Exercise 2: Accounts. Write an Account class with a balance and a protected constructor that receives it. Write two subclasses: SavingsAccount starts with 100, and CheckingAccount starts with 0. Put one of each in an Account[] and print the total balance.

Exercise 3: Percentages. Write a Percentage class with a private constructor and a read-only value. Add two static methods: fromPercent(50) and fromFraction(0.5). Both must throw an error if the result isn't between 0 and 100.

Exercise 4: One logger. Write a singleton Logger with a messages array and a log(message: string): void method. Get the instance twice, log one message through each, and print how many messages the second one has.

Exercise 5: Messages (challenge). Write an abstract class Message with a read-only text, an abstract method getChannel(): string and a normal method send(): string that returns text like Email: Hello. Write the subclasses EmailMessage and SmsMessage, with no constructors, and send one of each from a Message[].

Quick quiz

  1. What does protected before a constructor do?
  2. Can a subclass call a protected constructor with super?
  3. Why does class Car extends Vehicle {} fail when you call new Car()?
  4. Does a protected constructor stop new Vehicle() when the program runs?
  5. What's the difference between a protected and a private constructor?
  6. What's a factory method?
  7. What's a singleton?
  8. What can an abstract class do that a protected constructor can't?
  9. Why can a plain object still count as a Vehicle?

Answers

Exercise 1: Circle inherits the protected constructor, so both of the last two lines are rejected. tsc still writes main.js, and nothing stops the program:

1main.ts(14,13): error TS2674: Constructor of class 'Shape' is protected and only accessible within the class declaration.
2main.ts(15,13): error TS2674: Constructor of class 'Shape' is protected and only accessible within the class declaration.
3Square {}
4Circle {}
5Shape {}

Exercise 2: Each subclass passes its starting balance to super:

1class Account {
2  balance: number;
3
4  protected constructor(balance: number) {
5    this.balance = balance;
6  }
7}
8
9class SavingsAccount extends Account {
10  constructor() {
11    super(100);
12  }
13}
14
15class CheckingAccount extends Account {
16  constructor() {
17    super(0);
18  }
19}
20
21const accounts: Account[] = [new SavingsAccount(), new CheckingAccount()];
22const total = accounts.reduce((sum, account) => sum + account.balance, 0);
23
24console.log(total);
100

Exercise 3: fromFraction reuses fromPercent, so the check lives in one place:

1class Percentage {
2  readonly value: number;
3
4  private constructor(value: number) {
5    this.value = value;
6  }
7
8  static fromPercent(value: number): Percentage {
9    if (value < 0 || value > 100) {
10      throw new Error(`Invalid percentage: ${value}`);
11    }
12    return new Percentage(value);
13  }
14
15  static fromFraction(fraction: number): Percentage {
16    return Percentage.fromPercent(fraction * 100);
17  }
18}
19
20console.log(Percentage.fromPercent(50));
21console.log(Percentage.fromFraction(0.5));
22
23try {
24  Percentage.fromFraction(2);
25} catch (error) {
26  console.log(String(error));
27}
Percentage { value: 50 }
Percentage { value: 50 }
Error: Invalid percentage: 200

Exercise 4: Both variables point to the same object:

1class Logger {
2  private static instance?: Logger;
3  messages: string[] = [];
4
5  private constructor() {}
6
7  static getInstance(): Logger {
8    if (Logger.instance === undefined) {
9      Logger.instance = new Logger();
10    }
11    return Logger.instance;
12  }
13
14  log(message: string): void {
15    this.messages.push(message);
16  }
17}
18
19const first = Logger.getInstance();
20const second = Logger.getInstance();
21
22first.log("Started");
23second.log("Finished");
24
25console.log(second.messages.length);
2

Exercise 5: The subclasses inherit the public constructor, which is why they don't need one of their own:

1abstract class Message {
2  readonly text: string;
3
4  constructor(text: string) {
5    this.text = text;
6  }
7
8  abstract getChannel(): string;
9
10  send(): string {
11    return `${this.getChannel()}: ${this.text}`;
12  }
13}
14
15class EmailMessage extends Message {
16  override getChannel(): string {
17    return "Email";
18  }
19}
20
21class SmsMessage extends Message {
22  override getChannel(): string {
23    return "SMS";
24  }
25}
26
27const messages: Message[] = [new EmailMessage("Hello"), new SmsMessage("Hi")];
28
29for (const message of messages) {
30  console.log(message.send());
31}
Email: Hello
SMS: Hi

Quiz: (1) It stops code outside the class and its subclasses from calling new. (2) Yes. (3) With no constructor of its own, Car inherits the protected one. (4) No. It's a compile-time check, and the program still builds the object. (5) A protected constructor allows subclasses, and a private one forbids extending the class at all. (6) A static method that builds and returns an object, often after checking its input. (7) A class with exactly one instance, shared by everything that asks for it. (8) Declare methods with no body, which every subclass must write. (9) Types are compared by shape, and a constructor isn't part of an object's type.

Cheat sheet

I want to…Write
Allow only subclasses to be builtprotected constructor() {}
Make a subclass buildableconstructor() { super(); }
Forbid new and inheritanceprivate constructor() {}
Build through a named methodstatic fromCelsius(celsius: number): Temperature
Keep a single instanceprivate static instance?: Settings;
Declare a base-only classabstract class Vehicle { … }
Require a method in subclassesabstract getWheels(): number;
Check the class at run timeif (new.target === Vehicle) { … }
Accept a class you can buildVehicleClass: new () => Vehicle

Wrapping up

Constructor access decides who gets to call new:

  • A protected constructor allows super and blocks new from outside.
  • Each concrete subclass needs its own public constructor, or it inherits the protected one.
  • The check is compile-time only. Use new.target for a check at run time.
  • A private constructor also forbids inheritance.
  • Static methods can still call new, which gives you factories and singletons.
  • abstract is usually the best way to say "only a base class", and it can require methods.
  • Constructor access doesn't change an object's type, so a plain object can still fit.

Next up: TypeScript Protected Members.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 4 min read

Ruby Blocks and Iterators: Write Your Own

Learn how Ruby methods receive blocks, pass values with yield, handle a missing block, and return an Enumerator. Build and run your own iterator.

nbkhope
Programming 6 min read

C++ std::vector: Size, Capacity, and Safe Growth

Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

nbkhope
Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.