TypeScript Public, Private, and Protected Modifiers Step by step Implementation and Top 10 Questions and Answers
 .NET School AI Teacher - SELECT ANY TEXT TO EXPLANATION.    Last Update: April 01, 2025      21 mins read      Difficulty-Level: beginner

Understanding TypeScript Public, Private, and Protected Modifiers

TypeScript, a statically typed programming language built on JavaScript, extends the capabilities of plain JS by introducing powerful concepts like classes, interfaces, and access modifiers. Among these, public, private, and protected are critical access modifiers that control the visibility and accessibility of class members. Properly utilizing these modifiers can significantly enhance code organization, security, and reusability.

Public Modifier

The public modifier is the default access level for TypeScript class members if you don't specify any other access modifier. Public members can be accessed from anywhere in your code, both within and outside the class. This flexibility is useful but requires careful management to avoid exposing internal implementation details which could lead to code fragility.

Syntax:

class MyClass {
    public myPublicProperty: string = 'I am public';

    public myPublicMethod(): void {
        console.log('This is a public method');
    }
}

Example Usage:

const instance = new MyClass();
console.log(instance.myPublicProperty); // Outputs: I am public
instance.myPublicMethod();            // Outputs: This is a public method

Explanation:

  • Accessibility: Public properties and methods are accessible throughout the entire application. They can be accessed directly via an instance of the class.
  • Use Case: It's ideal for APIs where you need to expose functionality and state to other parts of your application or even external clients. For example, a Car class might have a public method startEngine() that can be called by anyone using an instance of the Car.

Private Modifier

The private modifier restricts the visibility of a class member to the class itself. Members declared as private cannot be accessed or modified outside of the declaring class. This encapsulation principle helps prevent misuse and unauthorized manipulation of internal data, making the system more robust and maintainable.

Syntax:

class MyClass {
    private myPrivateProperty: string = 'I am private';

    private myPrivateMethod(): void {
        console.log('This is a private method');
    }

    public accessPrivateMembers(): void {
        console.log(this.myPrivateProperty);
        this.myPrivateMethod();
    }
}

Example Usage:

const instance = new MyClass();

// The following lines would result in errors:
// console.log(instance.myPrivateProperty); 
// instance.myPrivateMethod();

// But you can still use a public method to indirectly access them:
instance.accessPrivateMembers();       // Outputs:
                                       // I am private
                                       // This is a private method

Explanation:

  • Accessibility: As the name suggests, private properties and methods are only accessible within the class they were defined in.
  • Use Case: Private members are typically used when there is data or functionality that should remain hidden from the rest of the application. In a banking app, you wouldn’t want the balance of an account to be modifiable directly from outside the Account class. Instead, you'd use public methods such as deposit() and withdraw() to safely update the state.

Protected Modifier

The protected modifier allows access to class members within its own class and derived (child) classes, but not directly from instances of the class. This is particularly useful when you want to enable subclasses to extend the functionality of a parent class while keeping certain aspects hidden from users of the parent class.

Syntax:

class ParentClass {
    protected myProtectedProperty: string = 'I am protected';

    protected myProtectedMethod(): void {
        console.log('This is a protected method');
    }
}

class ChildClass extends ParentClass {
    constructor() {
        super();
        console.log(this.myProtectedProperty);
        this.myProtectedMethod();
    }
}

Example Usage:

const childInstance = new ChildClass();
// The following will result in an error:
// console.log(childInstance.myProtectedProperty);  
// childInstance.myProtectedMethod();

// But the constructor of ChildClass can access them:

Explanation:

  • Accessibility: Protected members are accessible within the class they're defined in and any class that derives from it. They’re not visible to instances of the class.
  • Use Case: This is great when creating base classes designed for inheritance. Suppose you’re building a GUI framework with a base Control class that should handle some common operations and properties, but these shouldn't be exposed directly.

Practical Benefits of Access Modifiers

  1. Encapsulation: Ensures the internal representation of an object is hidden from the outside. External code interacts with objects through well-defined interfaces.

  2. Maintainability: Changes in the internal implementation of a class do not affect other parts of the application that rely on its external interface.

  3. Security: Prevents accidental changes to the internal state or functions, reducing the likelihood of bugs.

  4. Reusability: Allows you to create flexible and reusable class hierarchies. By carefully controlling what is exposed, you can build modular systems.

  5. Readability: Makes the code easier to understand by clearly defining the boundaries between internal implementation and external usage.

  6. Testing: Facilitates unit testing by restricting direct interaction with the internals of a class. Tests can focus on the externally observable behavior and interfaces.

  7. Code Documentation: Acts as self-documentation, indicating the intended purpose and design of your classes and their members.

When to Use Which Modifier

  • Public: Use when you need the property or method to be part of the public API of the class.
  • Private: Use when you want to keep the internal workings of the class hidden. Avoid exposing sensitive data or methods that could interfere with the class's integrity.
  • Protected: Use when you intend the class to be inherited, and wish to allow subclasses to use specific members, but keep them out of reach for end-users or non-derived classes.

In summary, TypeScript’s access modifiers (public, private, and protected) provide developers with tools to create well-structured, secure, and maintainable codebases. By properly applying these modifiers, you can ensure encapsulation, reduce code fragility, and promote best coding practices. Understanding the appropriate use of each modifier is crucial to leveraging these benefits effectively.




Examples, Set Route and Run the Application Then Data Flow: A Step-by-Step Guide with TypeScript Modifiers

TypeScript, being a statically typed superset of JavaScript, introduces several features that make it a favorite among developers working on large-scale applications. One such feature is access control modifiers, which are public, private, and protected. These modifiers allow you to restrict access to class properties and methods depending on where they are used.

Understanding Access Modifiers

  1. Public - Members are accessible from anywhere.
  2. Private - Members can only be accessed within the same class.
  3. Protected - Members are accessible within the same class and any subclasses derived from it.

Let’s explore these concepts with an example application built using Angular, which is a popular framework for developing client-side web applications with TypeScript.

Prerequisites

To follow along with this guide, you will need:

  1. Node.js & npm: Install Node.js and npm (Node Package Manager) from their official websites.
  2. Angular CLI: Install Angular CLI globally by running the following command in your terminal or command prompt:
    npm install -g @angular/cli
    

Step 1: Create New Angular Project

First, let’s create an Angular project. Open your terminal and execute:

ng new type-script-modifiers-app
cd type-script-modifiers-app

Step 2: Generate a Sample Component and Service

For demonstration purposes, let's generate a component and a service that we will use to showcase how public, private, and protected modifiers work.

ng generate component user-profile
ng generate service user-manager

This will create a user-profile.component.ts file for our component and a user-manager.service.ts file for our service.

Step 3: Define a Basic User Model with Modifiers

In our application, we'll have a simple user model with different access levels for each property. Let's define this model in a separate file first.

Create a User.ts file in a models folder:

mkdir src/app/models
touch src/app/models/User.ts

Now, add this code to src/app/models/User.ts:

export class User {
  public name: string;
  private email: string;
  protected age: number;

  constructor(name: string, email: string, age: number) {
    this.name = name;
    this.email = email;
    this.age = age;
  }

  public getEmail(): string {
    return this.email;
  }

  protected getAge(): number {
    return this.age;
  }
}
  • Name: Marked as public, meaning it can be accessed anywhere.
  • Email: Marked as private, meaning it can only be accessed inside the User class itself.
  • Age: Marked as protected, meaning it can only be accessed in the User class and its subclasses.

Step 4: Utilize the User Model in the UserManagerService

We'll now implement the UserManagerService which will use instances of User.

Open src/app/user-manager.service.ts and modify it as follows:

import { Injectable } from '@angular/core';
import { User } from './models/User';

@Injectable({
  providedIn: 'root'
})
export class UserManagerService {
  private users: User[] = [];

  constructor() {}

  addUser(user: User) {
    this.users.push(user);
  }

  public getUserNames(): string[] {
    return this.users.map(u => u.name);
  }

  // This method will show an error if uncommented because 'email' is private.
  // getUserEmails(): string[] {
  //   return this.users.map(u => u.email);
  // }

  getAverageAge(): number {
    const ages = this.users.map(u => (<any>u).getAge());  // Casting to any to force protected access, not recommended in practice.
    return ages.reduce((acc, curr) => acc + curr, 0) / ages.length;
  }
}

In the UserManagerService:

  • We maintain a private array of users.
  • The addUser method is public, allowing other classes to add users.
  • The getUserNames method is also public, retrieving names of all users.
  • We attempted to write a getUserEmails method but commented it out as it would result in a compile-time error since email is private.

Important Note

The <any> cast allows us to access protected methods but is not recommended in practice as it bypasses the intended encapsulation. Ideally, only protected subclasses should access protected members.

Step 5: Implement Subclassing to Show Protected Modifier Usage

Let’s create a subclass AdvancedUser that inherits from User to demonstrate access to protected members.

Create AdvancedUser.ts in the models folder:

touch src/app/models/AdvancedUser.ts

Add the below code in src/app/models/AdvancedUser.ts:

import { User } from './User';

export class AdvancedUser extends User {
  public displayUserInfo(): void {
    console.log(`Name: ${this.name}, Age: ${this.getAge()}`); // Accessible since 'name' is public and 'getAge()' is protected.
  }
}

Here, displayUserInfo() can access name and the protected method getAge() directly from the superclass without any issues.

Step 6: Update the UserProfileComponent to Use the Service

Next, we’ll use our UserManagerService in UserProfileComponent to add and retrieve user names.

Edit src/app/user-profile/user-profile.component.ts as shown below:

import { Component, OnInit } from '@angular/core';
import { UserManagerService } from '../user-manager.service';
import { User } from '../models/User';

@Component({
  selector: 'app-user-profile',
  templateUrl: './user-profile.component.html',
  styleUrls: ['./user-profile.component.css']
})
export class UserProfileComponent implements OnInit {
  private usersEmails!: string[];

  constructor(private userManagerService: UserManagerService) {}

  ngOnInit(): void {
    this.userManagerService.addUser(new User('Alice', 'alice@example.com', 28));
    this.userManagerService.addUser(new User('Bob', 'bob@example.com', 22));
    
    console.log('All User Names: ', this.userManagerService.getUserNames());

    const advancedUser = new AdvancedUser('Charlie', 'charlie@example.com', 30);

    advancedUser.displayUserInfo();
  }
}

In this component:

  • We inject UserManagerService to access its public methods and add users.
  • Inside ngOnInit, we add some users.
  • We log the user names.
  • We create an AdvancedUser instance that can utilize both public and protected parts of the User class.

Step 7: Set Up Routes to Access Your Component

Now we’ve a UserProfileComponent, but we need to set up routing so we can navigate to it.

Open src/app/app-routing.module.ts:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { UserProfileComponent } from './user-profile/user-profile.component';

const routes: Routes = [
  { path: 'user-profile', component: UserProfileComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

This setup will route requests to http://localhost:4200/user-profile to display the UserProfileComponent.

Step 8: Modify AppModule to Include UserProfileComponent

Let’s ensure that Angular knows the UserProfileComponent is a part of our application.

Open src/app/app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { UserProfileComponent } from './user-profile/user-profile.component';

@NgModule({
  declarations: [
    AppComponent,
    UserProfileComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Include the routed component in the declarations.

Step 9: Update the App Component Template to Navigate Using RouterLink

Finally, let’s update the home page template to include a navigation link pointing to our new component.

Edit src/app/app.component.html:

<h1>Welcome to the TypeScript Modifiers Example</h1>
<a routerLink="/user-profile">Go to User Profile Page</a>

<router-outlet></router-outlet>

Adding router-outlet in the template will ensure that the routed component is displayed on that outlet.

Step 10: Run the Application

With everything set up, let's run our application and see how it works.

Run the application using the Angular CLI:

ng serve

Navigate to http://localhost:4200 using your browser. Click the "Go to User Profile Page" link. You should see the console output from UserProfileComponent.

Data Flow Explanation

  1. Initialization: Angular boots up by loading modules. It encounters the AppModule and sets up components and services defined within.
  2. Routing: When the navigation link is clicked, Angular checks AppRoutingModule for the defined routes and navigates accordingly.
  3. Component Instantiation: Angular creates an instance of UserProfileComponent, injecting dependencies such as UserManagerService.
  4. Adding Users: Inside ngOnInit, it uses the public addUser method of UserManagerService to add user instances.
  5. Retrieving Data: The public getUserNames method is called, which internally accesses the private array of users and logs their names.
  6. Subclass Method Call: An AdvancedUser instance is created, which has access to both public and protected parts of User, demonstrating the functionality of protected modifiers.

Conclusion

Using public, private, and protected access modifiers in TypeScript helps in organizing your code logically. Public members are open for everyone, private members are confined to the class they belong to, and protected members can be accessed within the same class and any derived classes.

In this step-by-step guide, we've seen how to incorporate TypeScript modifiers into an Angular project, including creating a model, generating services and components, setting up routing, and understanding data flow.

Always remember that maintaining proper encapsulation leads to cleaner and more maintainable codebases. By restricting access appropriately, you avoid unintended modifications and keep the internal state consistent.




Certainly! Here’s a detailed overview of the top 10 questions and their answers related to TypeScript's public, private, and protected modifiers:

1. What are the differences between public, private, and protected modifers in TypeScript?

Answer: In TypeScript, access modifiers determine the visibility and accessibility of class members including properties, methods, and constructors.

  • Public (default): Members are accessible anywhere, both within the class, from instances of the class, and outside the class.
  • Private: Members are only accessible within the class itself. Instances of the class (or subclasses) cannot access them.
  • Protected: Members are accessible within the class and subclasses, but they cannot be accessed from outside the class or its subclasses.

2. When should you use public modifiers?

Answer: Public is the default access modifier. You should use public when you want to allow unrestricted access to a class member, both internally and externally.

Example:

class Vehicle {
    public name: string;

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

    public start(): void {
        console.log(`${this.name} has started.`);
    }
}

const car = new Vehicle("Toyota");
car.start(); // Toyota has started.
console.log(car.name); // Toyota

3. When should you use private modifiers?

Answer: Use private when you want to encapsulate the details of a class and ensure that the internal state is hidden and cannot be manipulated from outside the class.

Example:

class Car {
    private _speed: number;

    constructor() {
        this._speed = 0;
    }

    private accelerate(amount: number): void {
        this._speed += amount;
    }

    public start(): void {
        this.accelerate(10);
        console.log(`Car started at ${this._speed} km/h.`);
    }
}

const myCar = new Car();
myCar.start(); // Car started at 10 km/h.
// myCar._speed will give a compile-time error as it's private.

4. Explain the use of protected modifiers in TypeScript.

Answer: Protected is typically used when you want the method or property to be accessible within the class and its subclasses but not from outside the class hierarchy.

Example:

class Vehicle {
    protected engineType: string;

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

    protected startEngine(): void {
        console.log(`Starting engine of type ${this.engineType}.`);
    }
}

class Car extends Vehicle {
    constructor(engineType: string) {
        super(engineType);
    }

    public start(): void {
        this.startEngine();
        console.log("Car has started.");
    }
}

const myCar = new Car("V8");
myCar.start(); // Starting engine of type V8. Car has started.
// myCar.engineType will give a compile-time error as it's protected.

5. Can a class in TypeScript have a private constructor?

Answer: Yes, a class in TypeScript can have a private constructor. This prevents the class from being instantiated outside the class itself, essentially making the class non-instantiable. This is often used when you want a static method to create and manage instances, a design pattern known as the Singleton pattern.

Example:

class Logger {
    private static instance: Logger;

    private constructor() {
        // Initialization code...
    }

    public static getInstance(): Logger {
        if (!Logger.instance) {
            Logger.instance = new Logger();
        }
        return Logger.instance;
    }

    public log(message: string): void {
        console.log(message);
    }
}

const logger = Logger.getInstance();
logger.log("This is a log message.");
// new Logger() will give a compile-time error as the constructor is private.

6. Can you change the accessibility of a member in a subclass?

Answer: No, you cannot change the accessibility of a member from private to protected or public in a subclass. This ensures that the encapsulation provided by private and protected is not violated.

7. How does TypeScript enforce access control at runtime?

Answer: TypeScript primarily enforces access control at compile time, generating appropriate JavaScript code that respects the access modifiers. Since JavaScript does not natively support these access controls, TypeScript does not enforce them at runtime. This means that while TypeScript will prevent you from accessing private or protected members in your TypeScript code, this check is not performed at runtime, and the member can still be accessed if the JavaScript code is modified outside of TypeScript.

8. What if you need to access a private member for testing purposes?

Answer: While it's generally a bad practice to access private members for testing, there are a few approaches you can consider:

  • Refactor the Code: Improve the design so that such members are not private or are accessible via public or protected methods.
  • Use Type Assertions: Alternatively, you can use type assertions to bypass access modifiers during testing, but this is not recommended for regular project use.
  • Test Internals via Public API: Design your classes in a way that all necessary parts of the internal logic are indirectly tested via the public API.

9. How do public, private, and protected modifiers affect inheritance?

Answer: Public and Protected modifiers allow inheritance and polymorphism, whereas Private does not. Here are the effects:

  • Public: Any member marked as public can be accessed in all derived classes.
  • Protected: Members marked as protected are accessible in the derived classes but not outside the class hierarchy.
  • Private: Members marked as private are not accessible in derived classes or outside the class.

Example:

class Base {
    public basePublic: string;
    protected baseProtected: string;
    private basePrivate: string;

    constructor() {
        this.basePublic = "Public";
        this.baseProtected = "Protected";
        this.basePrivate = "Private";
    }

    public show(): void {
        console.log(`Public: ${this.basePublic}, Protected: ${this.baseProtected}, Private: ${this.basePrivate}`);
    }
}

class Derived extends Base {
    public accessBaseMembers(): void {
        console.log(`Public: ${this.basePublic}`); // Accessible
        console.log(`Protected: ${this.baseProtected}`); // Accessible
        // console.log(`Private: ${this.basePrivate}`); // Error: Property 'basePrivate' is private and only accessible within class 'Base'.
    }
}

const derived = new Derived();
derived.show(); // Public: Public, Protected: Protected, Private: Private
derived.accessBaseMembers(); // Public: Public, Protected: Protected

10. Can you use these access modifiers on class methods and constructors besides properties?

Answer: Yes, access modifiers can be applied to class methods and constructors in addition to properties.

  • Public Methods: Can be accessed from anywhere, both inside and outside the class.
  • Private Methods: Can only be accessed within the class itself.
  • Protected Methods: Can be accessed within the class and its derived classes.
  • Private Constructors: Prevents the class from being instantiated outside the class itself.

Example:

class Vehicle {
    private static instance: Vehicle;

    private constructor() {
        // Initialization code...
    }

    public static getInstance(): Vehicle {
        if (!Vehicle.instance) {
            Vehicle.instance = new Vehicle();
        }
        return Vehicle.instance;
    }

    private startEngine(): void {
        console.log("Starting engine...");
    }

    protected accelerate(speed: number): void {
        console.log(`Accelerating to ${speed} km/h.`);
    }

    public start(): void {
        this.startEngine();
        this.accelerate(100);
        console.log("Vehicle has started.");
    }
}

const vehicle = Vehicle.getInstance();
vehicle.start(); // Starting engine... Accelerating to 100 km/h. Vehicle has started.
// vehicle.startEngine() will give a compile-time error as it's private.

In summary, understanding TypeScript's public, private, and protected modifiers is crucial for creating well-structured and maintainable code. These modifiers help enforce encapsulation and enable proper access control to class members.