Page Content

Tutorials

What are the Access Modifiers in TypeScript?

Access Modifiers in TypeScript

To control a class member’s accessibility, TypeScript offers three different types of access modifiers: public, private, and protected. In the TypeScript type system, these modifiers only impose constraints during compilation; they have no effect during runtime in the JavaScript that is produced. The member is implicitly public if an access modifier is not used, following standard JavaScript practice. These modifications are applicable to both methods and properties.

  1. public: All individuals who are explicitly or tacitly designated as public are available to everyone. Instances made outside of the class hierarchy, as well as the defining class and its child classes, can access them. This is the visibility by default.
  2. protected: A protected member can be accessed by any classes that extend it (subclasses/children) as well as within the class in which it is declared. Importantly, they cannot be directly accessed through an instance object that was not made inside the class or its hierarchy.
  3. private: You can only access a private member from inside the defining class. Private members cannot be accessed by child classes or instances outside of the class.

Parameter Properties Shorthand

TypeScript provides Parameter Properties, a handy shortcut. If a constructor parameter is prefixed with readonly and/or an access modifier (public, private, or protected), TypeScript will automatically declare that parameter as an instance member of the class and initialise it with the argument that was supplied to the constructor.

Code Example: Access Modifiers

This example demonstrates the accessibility constraints of the three primary modifiers:

// TypeScript
class BaseVehicle {
    public brand: string;
    private serialNumber: string;
    protected maxSpeed: number;

    constructor(brand: string, serial: string, speed: number) {
        this.brand = brand;
        this.serialNumber = serial;
        this.maxSpeed = speed;
    }
    
    public getBrand() { return this.brand; } // OK
    protected getSpeed() { return this.maxSpeed; } // OK for children
    private getSerial() { return this.serialNumber; } // OK only inside this class
}

class DerivedCar extends BaseVehicle {
    displayInfo() {
        console.log(`Brand: ${this.brand}`); // OK: public member
        console.log(`Max Speed: ${this.maxSpeed}`); // OK: protected member accessible in subclass
        
        // this.serialNumber; // ERROR: private member not accessible outside BaseVehicle
        
        console.log(`Protected Method Output: ${this.getSpeed()}`); // OK: protected method accessible in subclass
        // this.getSerial(); // ERROR: private method
    }
}

const myVehicle = new BaseVehicle("Tesla", "X123", 250);

myVehicle.brand; // OK: public property
// myVehicle.serialNumber; // ERROR: private property not accessible on instance
// myVehicle.maxSpeed; // ERROR: protected property not accessible on instance

myVehicle.getBrand(); // OK: public method
// myVehicle.getSpeed(); // ERROR: protected method not accessible on instance
// myVehicle.getSerial(); // ERROR: private method

Output/Explanation:

If compiled, only the lines marked with // OK would pass the TypeScript compiler checks. Attempts to access serialNumber (private) or maxSpeed (protected) directly on myVehicle instances would result in compile-time errors.

Readonly Properties in Classes

Developers can designate a class property as immutable after it has been initially set by using the readonly modifier. The readonly keyword only applies to properties, but the const keyword in JavaScript applies to variable references.

A readonly property needs to be initialised in the class’s constructor or at the definition point. The property can only be read after initialisation; any later attempts to reassign or change it will result in a compile-time error.

When you designate a member as both readonly and having a visibility modifier, the visibility modifier (public readonly, for example) is displayed first.

Example: Readonly Properties in Code

// TypeScript 
class ImmutableConfig {
    // Initialized at declaration point
    public readonly ENVIRONMENT: "production" | "development" = "production";
    
    // Initialized in the constructor
    private readonly launchTime: number; 

    constructor(initialTime: number) {
        this.launchTime = initialTime; // OK: Initial assignment in constructor
    }

    // Attempting to modify a readonly property in a method fails
    modifyEnvironment(newEnv: "staging") {
        // this.ENVIRONMENT = newEnv; 
        // ERROR: Cannot assign to 'ENVIRONMENT' because it is a read-only property.
    }
    
    // Attempting to modify a private readonly property after initialization fails
    updateLaunchTime() {
        // this.launchTime = Date.now();
        // ERROR: Cannot assign to 'launchTime' because it is a read-only property.
    }

    getLaunchTime() {
        return this.launchTime; // OK: Reading is permitted
    }
}

const config = new ImmutableConfig(1672531200000);

// config.ENVIRONMENT = "staging"; 
// ERROR: Cannot assign to 'ENVIRONMENT' because it is a read-only property.

Output/Explanation:

The lines attempting to reassign ENVIRONMENT or launchTime outside of their permitted initialization locations would generate compile-time errors, ensuring immutability is respected within the TypeScript codebase.

Abstract Classes

Children must provide the implementation since the abstract keyword indicates that a class or class member cannot be directly utilised or invoked.

  1. Cannot Be Instantiated: It is not possible to instantiate an abstract class directly with the new keyword. Creating an instance requires first creating a concrete (non-abstract) class that inherits from it.
  2. Abstract Members: The abstract base class does not include an implementation body for an abstract member, which is typically a method. It is similar to an interface declaration in that it just supplies the method signature.
  3. Mandatory Implementation: Every concrete class that derives from an abstract class needs to give all of its inherited abstract members an implementation. The derived class needs to be tagged as abstract as well if it doesn’t.
  4. Hybrid Nature: Abstract classes can be thought of as a hybrid of an interface (which defines methods that inheriting classes must implement) and a regular class (which can provide specific methods with implementation bodies).

Example of Code: Abstract Class

// TypeScript
abstract class Animal {
    public readonly name: string;
    
    constructor(name: string) {
        this.name = name;
    }
    
    // Abstract method: must be implemented by concrete subclasses
    abstract makeSound(): string; 
    
    // Concrete method: has an implementation and is inherited
    walk() {
        return `${this.name} is walking.`;
    }
}

class Dog extends Animal {
    // Must implement makeSound()
    makeSound(): string {
        return "Woof! Woof!";
    }
}

// const pet = new Animal("Generic Pet"); 
// ERROR: Cannot create an instance of an abstract class. 

const buddy = new Dog("Buddy"); // OK: Dog is a concrete class
console.log(buddy.walk()); 
console.log(buddy.makeSound());

Output (Conceptual):

Buddy is walking.
Woof! Woof!

Simulating Final Classes

There is no native support for the idea of a “final” class (a class that cannot be expanded) in TypeScript. However, by utilising the private constructor access modifier, a design pattern may be used to mimic this behaviour.

By designating a class’s constructor as private, you stop other classes from extending it and, more importantly, external code from invoking new ClassName() directly.

A public static factory function must be provided to manage instance generation internally, as preventing instantiation typically negates the class’s purpose.

Code Example: Final Class Simulation

// TypeScript 
class FinalComponent {
    private id: string;
    
    // Private constructor prevents instantiation OR extension 
    private constructor(id: string) {
        this.id = id;
    }

    // Static factory method allows controlled instantiation
    public static createInstance(): FinalComponent {
        const uniqueId = Math.random().toString(36).substring(2, 9);
        return new FinalComponent(uniqueId);
    }
    
    public getID(): string {
        return this.id;
    }
}

// class AttemptedExtension extends FinalComponent {} 
// ERROR: Cannot extend a class 'FinalComponent'. Class constructor is marked as private. 

// const instance1 = new FinalComponent("A");
// ERROR: Constructor of class 'FinalComponent' is private and only accessible within the class declaration. 

const instance2 = FinalComponent.createInstance(); // OK: Uses the static factory method
console.log(instance2.getID());

Output (Conceptual):

s9a3e2k // Output will be a random unique ID string

The class’s usability is demonstrated by this successful instantiation via the static method, and its ability to simulate a “final” state is confirmed by the compile-time faults that preclude both direct instantiation and extension.

Index