Object-Oriented Programming (OOP) in TypeScript
Object-Oriented Programming in TypeScript builds directly on top of JavaScript's standard class syntax. By overlaying compile-time type annotations, access modifiers (public, private, protected), readonly properties, static members, and abstract classes, TypeScript transforms JavaScript's dynamic class structures into a predictable, statically validated architectural framework.
1. Class Declaration & Predictable Type Inference
In standard JavaScript, declaring properties on a class often happens implicitly inside the constructor. TypeScript requires explicit property declarations with type annotations inside the class body. This explicit contract provides full auto-completion (IntelliSense) and runtime predictability when consuming class instances.
Naive vs. Constructor-Based Class Definition
If properties are declared without initial values or constructor initialization, TypeScript raises strict property initialization warnings:
// Naive Approach: Missing initialization or constructor
class UnsafeChai {
flavor: string; // Error: Property 'flavor' has no initializer and is not definitely assigned in the constructor.
price: number;
}
// Consuming an uninitialized object offers predictive suggestions,
// but risks runtime undefined behavior if properties aren't assigned.
const masalaChai = new UnsafeChai();
masalaChai.flavor = "Masala";
2. Constructors & The Role of this
To guarantee that an object is fully formed upon creation, TypeScript enforces initialization inside a constructor. The this keyword inside a class dynamically points to the newly instantiated object instance.
class Chai {
flavor: string;
price: number;
constructor(flavor: string, price: number) {
this.flavor = flavor;
this.price = price;
}
}
// Instantiation requires exact argument types matching the constructor signature
const adrakChai = new Chai("Adrak", 20);
console.log(adrakChai.flavor); // "Adrak"
console.log(adrakChai.price); // 20
Conceptually: The
thiskeyword dynamically references whichever instance is currently being constructed or executing a method. Callingnew Chai("Adrak", 20)setsthisto point directly to theadrakChaiobject instance memory space.
3. Access Modifiers: public, private, and protected
Access modifiers control the visibility and accessibility of class properties and methods from outside or within class hierarchies.
Real-Life Analogy: Restaurant Access Control
Access Modifier Implementation Walkthrough
class Shop {
public flavor: string = "Masala Chai";
private secretIngredient: string = "Cardamom";
protected shopName: string = "Chai Corner";
// Public method to expose private data in a controlled manner
public revealSecret(): string {
return `Secret ingredient is: ${this.secretIngredient}`;
}
}
// Derived subclass extending Shop
class Branch extends Shop {
public getShopName(): string {
// 'protected' property accessible inside derived class
return this.shopName;
}
/*
public getSecret(): string {
// ERROR: Property 'secretIngredient' is private and only
// accessible within class 'Shop'.
return this.secretIngredient;
}
*/
}
const myBranch = new Branch();
console.log(myBranch.flavor); // Allowed (public)
console.log(myBranch.getShopName()); // Allowed (via public wrapper calling protected member)
console.log(myBranch.revealSecret()); // Allowed (via public wrapper calling private member)
// External instance direct access check:
// console.log(myBranch.shopName); // ERROR: Property 'shopName' is protected.
// console.log(myBranch.secretIngredient); // ERROR: Property 'secretIngredient' is private.
4. TypeScript private Keyword vs. JavaScript Native Private Fields (#)
TypeScript provides two distinct ways to define private variables: the TS-specific private keyword and the ES2020 standard JavaScript # private field syntax.
class Wallet {
// Standard ES2020 native private field
#balance: number = 1000;
// TypeScript private keyword (preferred by standard TS conventions)
private pinCode: number = 4321;
public getBalance(): number {
return this.#balance;
}
public getPin(): number {
return this.pinCode;
}
}
const myWallet = new Wallet();
// myWallet.#balance; // Compiler & Runtime Error
// myWallet.pinCode; // Compile-time TS Error (Erased in emitted JS)
Comparison Matrix
Note: While TypeScript supports
#field, using the explicitprivatekeyword is generally preferred in TypeScript codebases for cleaner syntax and consistent tooling integration.
5. Immutable State with readonly Properties
The readonly modifier prevents modification of a property after its initial assignment. Assignments are restricted to either the property declaration itself or within the class constructor.
class TeaCup {
// Immutable property set once
public readonly capacity: number = 250; // 250 ml
constructor(initialCapacity?: number) {
if (initialCapacity) {
this.capacity = initialCapacity; // Allowed inside constructor
}
}
public updateCapacity(): void {
// this.capacity = 350;
// ERROR: Cannot assign to 'capacity' because it is a read-only property.
}
}
const standardCup = new TeaCup(300);
console.log(standardCup.capacity); // 300
// standardCup.capacity = 500; // ERROR at compile time
6. Encapsulation via Controlled Getters and Setters
Getters (get) and Setters (set) allow intercepting reading and writing of class properties, executing validation or custom logic prior to property mutation.
Convention: The Underscore (_) Prefix
When backing private properties with getters and setters, developers conventionally prefix the private variable with an underscore (_sugar) to distinguish it from the public accessor method name (sugar).
class ModernChai {
private _sugar: number = 2; // Default 2 spoons of sugar
// Getter for sugar level
get sugar(): number {
return this._sugar;
}
// Setter with custom validation logic
set sugar(value: number) {
if (value > 5) {
throw new Error("Too sweet! Sugar value cannot exceed 5 spoons.");
}
this._sugar = value;
}
}
const myChai = new ModernChai();
console.log(myChai.sugar); // 2 (Invokes getter)
myChai.sugar = 4; // Updates via setter successfully
console.log(myChai.sugar); // 4
// Triggering validation guard in setter:
// myChai.sugar = 10; // Throws Runtime Error: "Too sweet! Sugar value cannot exceed 5 spoons."
7. Static Members for Class-Level Utility
Static properties and methods belong to the class itself rather than to any specific object instance created from the class. They are accessed directly through the class constructor name.
class ChaiCafe {
// Static property shared across all callers
public static shopName: string = "ChaiCode Cafe";
public flavor: string;
constructor(flavor: string) {
this.flavor = flavor;
}
public static printWelcomeMessage(): void {
console.log(`Welcome to ${ChaiCafe.shopName}!`);
}
}
// Accessed directly via Class Name:
console.log(ChaiCafe.shopName); // "ChaiCode Cafe"
ChaiCafe.printWelcomeMessage(); // "Welcome to ChaiCode Cafe!"
const individualOrder = new ChaiCafe("Ginger");
// individualOrder.shopName;
// ERROR: Property 'shopName' does not exist on type 'ChaiCafe'. Did you mean to access the static member 'ChaiCafe.shopName'?
8. Abstract Classes and Contract Enforcement
An abstract class serves as a baseline template or contract that cannot be instantiated directly using new. Abstract classes define method signatures without implementations (abstract make(): void), forcing derived child classes to implement those methods.
abstract class Drink {
// Abstract method declaration (no method body)
abstract make(): void;
// Concrete utility method available to all subclasses
public info(): void {
console.log("This is a beverage item.");
}
}
// Direct instantiation is blocked by compiler:
// const genericDrink = new Drink();
// ERROR: Cannot create an instance of an abstract class.
class MyChai extends Drink {
// Mandatory implementation of the abstract signature
public make(): void {
console.log("Brewing fresh tea leaves with milk and spices.");
}
}
const customizedChai = new MyChai();
customizedChai.make(); // "Brewing fresh tea leaves with milk and spices."
customizedChai.info(); // "This is a beverage item."
9. Code Reuse Architecture: Inheritance vs. Composition
While Inheritance (extends) models an "is-a" relationship (e.g., Branch is a Shop), Composition models a "has-a" relationship by embedding instances of other classes as internal dependencies.
Composition Code Walkthrough
In composition, a class delegates specific capabilities to separate helper classes instead of inheriting them.
class Heater {
public heat(): void {
console.log("Heating water to boiling temperature...");
}
}
class ChaiMaker {
// Private instance variable holding another class instance
private heater: Heater;
constructor() {
// Composition: ChaiMaker instantiates and owns a Heater instance
this.heater = new Heater();
}
public makeTea(): void {
// Delegate operation to the internal dependency
this.heater.heat();
console.log("Tea steeped and ready to serve!");
}
}
const maker = new ChaiMaker();
maker.makeTea();
// Output:
// "Heating water to boiling temperature..."
// "Tea steeped and ready to serve!"
Summary and Key Takeaways
- Explicit Typing: TypeScript classes require explicit type declarations for class-level properties to enable type checking and IDE auto-completion.
- Access Modifier Control: Visibility is strictly governed using
public(default),private(internal class access only), andprotected(internal class and derived subclass access). privateKeyword vs#Syntax: Theprivatekeyword offers compile-time checks with clean syntax, while#enforces runtime privacy in native JavaScript.- Immutable Properties: Applying
readonlylocks property reassignment after initialization in the constructor or property declaration. - Encapsulation with Accessors: Getters (
get) and Setters (set) protect private properties (conventionally named with an_prefix) by allowing validation rules prior to assignment. - Static Scope:
staticproperties and methods belong directly to the class constructor itself, rather than individual instances. - Abstract Contracts:
abstractclasses enforce method implementation rules on subclasses while blocking direct instantiation of the abstract base class. - Architectural Flexibility: Modern TypeScript design patterns favor Composition ("has-a") over strict Inheritance ("is-a") to create loosely coupled, maintainable modular systems.