Access Modifiers in TypeScript

In object-oriented programming, one of the most important concepts is encapsulation. This is the practice of bundling data and the methods that operate on that data into a single unit (a class) and restricting access to the inner workings of that class. Access modifiers in TypeScript are the tools that allow you to do exactly this.

By using access modifiers, you define a "contract" for how other parts of your code can interact with your objects. This prevents accidental data corruption and makes your codebase much easier to maintain as it grows.

 

Key Access Modifiers:

  1. public: The "open door" policy.
  2. private: Strictly for internal use only.
  3. protected: Shared only with family (subclasses).
Developer Tip: In TypeScript, if you don't explicitly add a modifier to a property or method, it defaults to public. This is different from languages like Java or C#, where defaults can vary.

public

The public modifier is the default state for class members. A public property or method can be accessed from anywhere—inside the class, by instances of the class, and by any other part of your application. While it is the default, some developers choose to explicitly write public to make their intentions clear to other teammates.

Example:

class Car {
  public make: string;
  public model: string;

  constructor(make: string, model: string) {
    this.make = make;
    this.model = model;
  }

  public displayInfo(): void {
    console.log(`Make: ${this.make}, Model: ${this.model}`);
  }
}

const myCar = new Car("Toyota", "Corolla");
console.log(myCar.make);  // Accessible outside the class
myCar.displayInfo();  // Accessible outside the class

In this example, because make, model, and displayInfo() are public, we can interact with them directly after creating a new Car. This is useful for data that needs to be visible to the rest of your app, such as a user's display name or a product's price.

Best Practice: Even though it's the default, explicitly marking methods as public can improve code readability, especially in large projects where you want to distinguish between the public API of a class and its internal logic.

private

The private modifier is your primary tool for hiding complexity. When a member is marked as private, it is only visible within the class where it was defined. Even subclasses cannot touch it. This is perfect for "helper" methods or internal state that shouldn't be messed with by outside code.

Example:

class Employee {
  private id: number;
  private name: string;
  private salary: number;

  constructor(id: number, name: string, salary: number) {
    this.id = id;
    this.name = name;
    this.salary = salary;
  }

  private calculateTax(): number {
    return this.salary * 0.2; // Internal logic
  }

  public getTakeHomePay(): number {
    return this.salary - this.calculateTax(); // Accessing private method internally
  }
}

const emp = new Employee(1, "John", 50000);
// console.log(emp.salary);  // Error: 'salary' is private
// emp.calculateTax();       // Error: 'calculateTax' is private
console.log(emp.getTakeHomePay()); // Valid: public method calls the private logic

In this scenario, we don't want someone to accidentally change an employee's salary from outside the class or manually call calculateTax. We "expose" only what is necessary (getTakeHomePay) and hide the rest.

Watch Out: TypeScript's access modifiers are only enforced at compile-time. Once the code is transpiled to JavaScript, private and protected keywords disappear. If you need true runtime privacy, consider using the modern JavaScript #privateField syntax.
Common Mistake: Trying to access a private property using this inside a subclass. If Manager extends Employee, the Manager class cannot access this.salary if it is marked as private in the parent.

protected

The protected modifier sits right in the middle. It acts like private because it prevents access from outside the class, but it acts like public for subclasses. Use protected when you want to allow child classes to use or override a property, but still keep it hidden from the "outside world."

Example:

class Animal {
  protected species: string;

  constructor(species: string) {
    this.species = species;
  }
}

class Dog extends Animal {
  public breed: string;

  constructor(species: string, breed: string) {
    super(species);
    this.breed = breed;
  }

  public getIdentity(): string {
    // We can access 'species' here because Dog is a subclass of Animal
    return `I am a ${this.species} of breed ${this.breed}`;
  }
}

const myDog = new Dog("Canine", "Bulldog");
// console.log(myDog.species); // Error: Property 'species' is protected
console.log(myDog.getIdentity()); // Valid: Subclass method uses the protected property

By using protected, we ensure that while a Dog knows it is a "Canine," a random piece of code elsewhere in the app cannot change the species of our dog instance.

Developer Tip: Use protected for methods that provide "base functionality" intended to be customized or extended by child classes, such as a base render() logic in a UI component.

Readonly

While not strictly an "access" modifier in the sense of visibility, readonly is a powerful "mutation" modifier. It allows you to make a property immutable. Once a readonly property is assigned a value (either at the point of declaration or inside the constructor), it can never be changed again.

Example:

class Circle {
  readonly pi: number = 3.14159;
  readonly radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }
}

const circle = new Circle(10);
console.log(circle.radius);  // Output: 10
// circle.radius = 20;       // Error: Cannot assign to 'radius' because it is read-only.
Best Practice: Use readonly for configuration values, ID strings, or any data that should remain constant for the entire lifecycle of the object. It makes your code much more predictable.

 

Summary

Mastering access modifiers is key to writing clean, professional TypeScript. They act as the "API" documentation for your classes, telling other developers what they should and shouldn't touch.

  • public: The default. Visible everywhere. Use for the main features of your class.
  • private: Only visible inside the class. Use for "secret" internal logic and state.
  • protected: Visible inside the class and its children. Use for shared logic in inheritance.
  • readonly: Prevents the value from being changed after the object is created.
One last tip: You can use a shorthand in constructors to define and initialize properties at once: constructor(private id: number) {}. This creates the property, sets its access level, and assigns the value automatically!