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.
public
: All individuals who are explicitly or tacitly designated aspublic
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.protected
: Aprotected
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.private
: You can only access aprivate
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.
- 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. - Abstract Members: The abstract base class does not include an implementation body for an
abstrac
t member, which is typically a method. It is similar to an interface declaration in that it just supplies the method signature. - 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. - 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.