Home Blog Page 38

Abstract Classes and Interface Implementation in TypeScript

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What is an Abstract Class?
    • Defining an Abstract Class
    • Abstract Methods in TypeScript
    • When to Use Abstract Classes
  • What is an Interface?
    • Defining an Interface
    • Implementing an Interface
    • When to Use Interfaces
  • Comparing Abstract Classes and Interfaces
    • Key Differences
    • When to Choose Each One
  • Practical Examples
    • Example 1: Abstract Class
    • Example 2: Interface Implementation
  • Conclusion

Introduction

In TypeScript, both abstract classes and interfaces are essential tools for defining and enforcing structures and behaviors for objects. While they share some similarities, they have different use cases and provide distinct features. Understanding the differences between abstract classes and interfaces is crucial for building maintainable and scalable TypeScript applications.

This article dives deep into abstract classes and interfaces, explaining how they work in TypeScript, their differences, and when to use each. By the end, you’ll have a clear understanding of how to implement and choose between abstract classes and interfaces in your TypeScript code.


What is an Abstract Class?

An abstract class is a class that cannot be instantiated on its own. It serves as a blueprint for other classes. Abstract classes are meant to be extended by other classes, and they allow you to define both fully implemented methods and methods that must be implemented by subclasses.

Defining an Abstract Class

In TypeScript, you define an abstract class using the abstract keyword. An abstract class can contain both abstract methods (without implementations) and regular methods (with implementations).

abstract class Animal {
// Regular method
move(): void {
console.log("Moving...");
}

// Abstract method
abstract makeSound(): void;
}

Abstract Methods in TypeScript

Abstract methods are methods that don’t have a body and must be implemented by subclasses. These methods are defined using the abstract keyword, and they provide the required signature without implementation.

abstract class Animal {
abstract makeSound(): void;
}

When to Use Abstract Classes

Abstract classes are typically used when:

  • You want to provide shared behavior (methods) across multiple subclasses.
  • You want to enforce that certain methods must be implemented in subclasses (via abstract methods).
  • You need to define default behavior that can be shared across child classes, while still allowing some flexibility for customization.

An abstract class is particularly useful when the base class itself should not be instantiated, but you want to provide common functionality to derived classes.


What is an Interface?

An interface defines a contract for a class or object. It is a blueprint that ensures any class or object that implements it follows the defined structure. Unlike abstract classes, interfaces can only define method and property signatures and cannot contain any implementation.

Defining an Interface

In TypeScript, interfaces are defined using the interface keyword. Interfaces allow you to define properties and methods that any implementing class or object must provide.

interface Animal {
move(): void;
makeSound(): void;
}

Implementing an Interface

A class implements an interface by using the implements keyword. The class must then provide concrete implementations for all the methods defined in the interface.

class Dog implements Animal {
move(): void {
console.log("Dog is moving");
}

makeSound(): void {
console.log("Woof");
}
}

When to Use Interfaces

Interfaces are used when:

  • You want to define a contract for a class to follow, ensuring that the class implements specific methods and properties.
  • You need to allow multiple types to share a common structure without enforcing a strict inheritance chain (since a class can implement multiple interfaces).
  • You want flexibility and the ability to define common behavior across various classes without enforcing shared code or a class hierarchy.

Interfaces are ideal when you are concerned with ensuring that different classes share the same structure and behavior, regardless of where they exist in the class hierarchy.


Comparing Abstract Classes and Interfaces

Key Differences

Here’s a breakdown of the key differences between abstract classes and interfaces:

FeatureAbstract ClassesInterfaces
InstantiationCannot be instantiated directlyCannot be instantiated directly
MethodsCan contain both fully implemented methods and abstract methodsCan only declare method signatures (no implementations)
ConstructorCan have a constructorCannot have a constructor
Multiple InheritanceCan only inherit from one classCan implement multiple interfaces
Use CaseUse when sharing behavior and defining base functionalityUse for defining contracts (structure) to be followed by implementing classes

When to Choose Each One

  • Abstract Class: Choose an abstract class when you need to share code and functionality among multiple subclasses and when you have a common base class that should not be instantiated directly. Example Use Case: If you’re creating a framework for animals and want all animals to have some shared behavior like move(), but want to enforce that specific animals (like Dog or Cat) implement their own version of makeSound().
  • Interface: Choose an interface when you want to define a contract without enforcing implementation details. Interfaces are great when multiple classes need to share the same structure but may implement the behavior in different ways. Example Use Case: If you have different classes like Car, Boat, and Airplane, all of which can implement a Driveable interface to ensure that each class has a drive() method, but you don’t care how each class implements the method.

Practical Examples

Example 1: Abstract Class

Let’s create a base class for different types of Shape, where common behavior like getArea() is shared, but the exact implementation of getArea() differs based on the type of shape.

abstract class Shape {
abstract getArea(): number;

displayArea(): void {
console.log(`Area: ${this.getArea()}`);
}
}

class Circle extends Shape {
radius: number;

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

getArea(): number {
return Math.PI * this.radius * this.radius;
}
}

class Square extends Shape {
side: number;

constructor(side: number) {
super();
this.side = side;
}

getArea(): number {
return this.side * this.side;
}
}

// Usage
const circle = new Circle(5);
circle.displayArea(); // Area: 78.53981633974483

const square = new Square(4);
square.displayArea(); // Area: 16

Here, the abstract class Shape defines a contract for all shapes to calculate their area, while the subclasses provide specific implementations for different shape types.

Example 2: Interface Implementation

In this example, we define an interface for a Driveable object and implement it in different classes:

interface Driveable {
drive(): void;
}

class Car implements Driveable {
drive(): void {
console.log("Car is driving");
}
}

class Truck implements Driveable {
drive(): void {
console.log("Truck is driving");
}
}

// Usage
const car = new Car();
car.drive(); // Car is driving

const truck = new Truck();
truck.drive(); // Truck is driving

In this case, the Driveable interface ensures that both Car and Truck implement the drive() method. The exact behavior of the drive() method can vary between different classes.


Conclusion

In TypeScript, abstract classes and interfaces are powerful tools for structuring and enforcing object-oriented designs. Abstract classes allow you to define shared behavior and ensure consistency across derived classes, while interfaces provide a flexible contract for ensuring that different classes implement specific methods.

When choosing between abstract classes and interfaces, consider whether you need shared behavior (abstract class) or just a common structure (interface). By understanding their differences and use cases, you can make more informed design decisions and write more maintainable, scalable code.

Composition over Inheritance in TypeScript

0
typscript course
typscript course

Table of Contents

  • Introduction
  • Understanding Inheritance and Composition
  • Why Prefer Composition Over Inheritance?
  • Composition in TypeScript
    • Using Interfaces and Classes for Composition
    • Composition Example: Combining Behaviors
    • Benefits of Composition
  • When to Use Inheritance
  • Conclusion

Introduction

In object-oriented programming, inheritance has traditionally been the go-to design pattern for code reuse and creating relationships between classes. However, over time, developers have recognized the limitations and drawbacks of inheritance, such as tight coupling and limited flexibility. This has led to the adoption of an alternative design principle known as Composition over Inheritance.

In this article, we will explore the concept of composition, why it’s often preferred over inheritance in TypeScript, and how you can implement composition in TypeScript. We’ll also discuss situations where inheritance might still be appropriate, and how to make the best choice depending on your project’s requirements.


Understanding Inheritance and Composition

Inheritance

Inheritance allows a class (child class) to inherit properties and methods from another class (parent class). It promotes reusability and can help establish relationships between entities. However, inheritance comes with certain drawbacks:

  • Tight Coupling: Child classes are tightly coupled to their parent classes, making changes in the parent class potentially break the child class.
  • Limited Flexibility: A child class can inherit only from one parent class, limiting how reusable and flexible the design can be.
  • Overriding Issues: When extending a class, you may end up overriding methods that might not be ideal for the specific use case of the child class.

Composition

Composition, on the other hand, involves creating more flexible, reusable components by combining simple objects or classes to achieve desired behaviors. Instead of extending a parent class, a class or object can include (compose) instances of other classes or interfaces that provide additional functionality.

Composition offers several advantages:

  • Loose Coupling: Components can be changed or replaced independently.
  • Flexibility: Different behaviors can be composed by mixing different components together.
  • Avoids Inheritance Chains: Instead of deep inheritance hierarchies, components can be reused directly, leading to better scalability.

Why Prefer Composition Over Inheritance?

While inheritance can be useful in certain scenarios, it often leads to rigid and less flexible code, especially in large applications. Below are some of the key reasons why composition is often preferred over inheritance:

1. Loose Coupling

With inheritance, subclasses are tightly coupled to their parent classes. If the parent class changes, it may require changes to all the child classes, even if they don’t require those changes. Composition, on the other hand, allows objects to interact with each other without tightly binding their implementations.

2. Avoiding Deep Inheritance Hierarchies

In larger applications, inheritance can lead to deep and complicated class hierarchies, making code hard to understand, extend, and maintain. With composition, you can avoid such complexities by composing objects from simpler building blocks.

3. Reusability and Flexibility

Inheritance allows reusability only in one direction (from parent to child). Composition allows more flexibility by enabling objects to share behaviors dynamically. You can mix and match different components and behaviors without affecting other parts of the codebase.

4. Encapsulation

In composition, each component can be independently developed, tested, and maintained. This leads to better encapsulation of functionality. In contrast, inheritance exposes inherited properties and methods, which may not be relevant to every subclass.


Composition in TypeScript

Composition in TypeScript typically involves using interfaces and classes to combine functionalities. Here are some approaches to implementing composition.

Using Interfaces and Classes for Composition

You can use interfaces to define reusable behavior and then compose these interfaces into a class. This approach allows a class to exhibit multiple behaviors without having to inherit from a single parent class.

Example: Using Composition for Combining Behaviors

Let’s create a scenario where a Car class needs to combine the behaviors of both Driveable and Flyable objects.

// Define two interfaces with different behaviors
interface Driveable {
drive(): void;
}

interface Flyable {
fly(): void;
}

// Create concrete implementations of the interfaces
class Car implements Driveable {
drive(): void {
console.log("Car is driving");
}
}

class Plane implements Flyable {
fly(): void {
console.log("Plane is flying");
}
}

// Create a class that uses composition to combine behaviors
class FlyingCar implements Driveable, Flyable {
private car: Car;
private plane: Plane;

constructor() {
this.car = new Car();
this.plane = new Plane();
}

drive(): void {
this.car.drive(); // Delegate to the car's drive method
}

fly(): void {
this.plane.fly(); // Delegate to the plane's fly method
}
}

// Usage of the composed class
const flyingCar = new FlyingCar();
flyingCar.drive(); // Output: Car is driving
flyingCar.fly(); // Output: Plane is flying

In this example:

  • The FlyingCar class doesn’t need to inherit from Car or Plane. Instead, it composes these objects to delegate the functionality.
  • This allows for more flexibility as you can easily swap out or modify the Car or Plane behaviors without affecting the FlyingCar class itself.

Composition Example: Composing Object Behaviors

Composition can also be used when you want to compose behaviors using simple objects.

// Define behaviors using functions
const canDrive = {
drive: () => console.log("Can drive!"),
};

const canFly = {
fly: () => console.log("Can fly!"),
};

// Compose objects with different behaviors
class FlyingCar {
private behaviors: any;

constructor() {
this.behaviors = Object.assign({}, canDrive, canFly);
}

drive(): void {
this.behaviors.drive();
}

fly(): void {
this.behaviors.fly();
}
}

// Usage
const flyingCar = new FlyingCar();
flyingCar.drive(); // Output: Can drive!
flyingCar.fly(); // Output: Can fly!

Here:

  • Instead of creating a separate Car and Plane object, we directly compose behaviors using simple objects that provide the drive and fly methods.
  • This allows more dynamic composition and flexibility.

Benefits of Composition

1. Separation of Concerns

Composition allows you to separate different concerns and encapsulate them in distinct components. This makes your code more modular and easier to maintain. For example, you can change the behavior of driving or flying without modifying the entire class structure.

2. Greater Flexibility

With composition, you can mix and match components that implement different behaviors. You’re not restricted to a rigid inheritance structure and can easily add new behaviors or replace existing ones as your system evolves.

3. Better Testability

Composition makes unit testing easier because each component can be tested independently. Unlike inheritance, where changes in the parent class can ripple through the entire class hierarchy, composed components are isolated and can be tested independently.

4. Avoiding Inheritance Pitfalls

Composition sidesteps many issues that come with inheritance, such as the diamond problem and deep inheritance trees. Instead of relying on complex relationships, composition gives you more control over how behaviors are combined.


When to Use Inheritance

While composition is often preferred, there are still situations where inheritance might be more appropriate:

  • When a clear “is-a” relationship exists: Inheritance works best when there’s a clear hierarchical relationship between classes. For example, a Dog class can inherit from an Animal class because a dog is an animal.
  • When you need to override or extend a base class’s functionality: If you have a base class that provides a lot of useful behavior and you need to customize it, inheritance is a natural choice.

In general, you should prefer composition when the relationship between components is “has-a” rather than “is-a.”


Conclusion

Composition over inheritance is a powerful design principle that leads to more maintainable, flexible, and scalable code. In TypeScript, you can leverage interfaces and classes to compose behaviors dynamically, avoiding the limitations of deep inheritance hierarchies. Composition encourages loose coupling, better testability, and greater flexibility when adding new behaviors or changing existing ones.

While inheritance is still a valid tool in certain scenarios, composition is often the better choice for building complex systems that need to be flexible and adaptable to change.

Inheritance and Class Hierarchies in TypeScript

0
typscript course
typscript course

Table of Contents

  • Introduction
  • What is Inheritance in TypeScript?
  • Single Inheritance in TypeScript
    • Extending Classes
    • Overriding Methods
  • The super Keyword in Inheritance
  • Multiple Inheritance (Simulated through Interfaces)
  • Class Hierarchies: Building Relationships Between Classes
  • Abstract Classes and Their Role in Inheritance
  • Constructor Inheritance and Initialization
  • Conclusion

Introduction

Inheritance is one of the core concepts of object-oriented programming (OOP) that allows a class to inherit properties and methods from another class. In TypeScript, inheritance helps create hierarchical relationships between classes, making it possible to reuse and extend existing code. TypeScript builds upon JavaScript’s prototypal inheritance by introducing features such as access modifiers, abstract classes, and interfaces to provide a more structured approach to inheritance.

In this article, we will dive deep into inheritance and class hierarchies in TypeScript, explaining how inheritance works, how to extend classes, override methods, and simulate multiple inheritance. We will also discuss the role of abstract classes and how to implement constructor inheritance.


What is Inheritance in TypeScript?

Inheritance is a mechanism where a new class (called a subclass or child class) derives properties and methods from an existing class (called a superclass or parent class). The subclass can inherit the behavior of the parent class and also define additional properties or override the inherited methods.

In TypeScript, inheritance is achieved using the extends keyword. A child class can extend a parent class, gaining access to its public and protected members.

Example of Inheritance

class Animal {
name: string;

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

speak(): void {
console.log(`${this.name} makes a sound.`);
}
}

class Dog extends Animal {
constructor(name: string) {
super(name); // Calls the constructor of the parent class
}

speak(): void {
console.log(`${this.name} barks.`);
}
}

const dog = new Dog("Buddy");
dog.speak(); // Output: Buddy barks.

In this example:

  • The Dog class extends the Animal class.
  • The Dog class inherits the name property and speak() method from Animal.
  • The Dog class overrides the speak() method to provide a custom implementation for dogs.

Single Inheritance in TypeScript

TypeScript supports single inheritance, where a class can only inherit from one parent class. However, TypeScript allows you to simulate multiple inheritance through interfaces, which we’ll cover later.

Extending Classes

When a class extends another, it inherits the properties and methods of the parent class. However, it cannot inherit private members of the parent class. Only public and protected members are accessible in the child class.

Example of Extending a Class

class Person {
protected name: string;

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

greet(): void {
console.log(`Hello, ${this.name}!`);
}
}

class Employee extends Person {
private jobTitle: string;

constructor(name: string, jobTitle: string) {
super(name); // Calls the constructor of Person
this.jobTitle = jobTitle;
}

displayJobTitle(): void {
console.log(`${this.name} is a(n) ${this.jobTitle}.`);
}
}

const employee = new Employee("John", "Software Engineer");
employee.greet(); // Output: Hello, John!
employee.displayJobTitle(); // Output: John is a(n) Software Engineer.

Here:

  • The Employee class extends Person and inherits the name property and greet() method.
  • The Employee class introduces a new method, displayJobTitle(), and a private jobTitle property.
  • The super() function is used in the Employee constructor to call the parent class’s constructor.

The super Keyword in Inheritance

The super keyword plays a critical role in class inheritance. It allows you to access the parent class’s methods and properties, as well as call its constructor.

Using super in Constructor

In the example above, the super(name) call inside the Employee class constructor is used to invoke the Person class’s constructor, ensuring the name property is correctly initialized.

Using super to Call Methods

You can also use super to call methods defined in the parent class. This is useful when you want to override a method but still keep the behavior from the parent class.

Example of Using super to Call Methods

class Animal {
sound(): void {
console.log("Animal makes a sound");
}
}

class Dog extends Animal {
sound(): void {
super.sound(); // Call the parent class's method
console.log("Dog barks");
}
}

const dog = new Dog();
dog.sound(); // Output: Animal makes a sound \n Dog barks

In this example:

  • The Dog class overrides the sound() method but calls the parent class’s sound() method using super.

Multiple Inheritance (Simulated through Interfaces)

TypeScript does not support multiple inheritance of classes. A class can only extend one parent class. However, TypeScript allows you to achieve multiple inheritance through interfaces.

A class can implement multiple interfaces, inheriting the contract of each interface. This is how TypeScript simulates multiple inheritance.

Example of Multiple Inheritance Using Interfaces

interface Flyable {
fly(): void;
}

interface Swimmable {
swim(): void;
}

class Duck implements Flyable, Swimmable {
fly(): void {
console.log("Duck is flying");
}

swim(): void {
console.log("Duck is swimming");
}
}

const duck = new Duck();
duck.fly(); // Output: Duck is flying
duck.swim(); // Output: Duck is swimming

In this example:

  • The Duck class implements two interfaces: Flyable and Swimmable.
  • This allows the Duck class to have both fly() and swim() methods.

Class Hierarchies: Building Relationships Between Classes

Class hierarchies allow you to establish relationships between classes. This is useful when you have a base class that contains common functionality, and derived classes that add or modify specific behavior.

Example of Class Hierarchy

class Vehicle {
move(): void {
console.log("Vehicle is moving");
}
}

class Car extends Vehicle {
move(): void {
console.log("Car is driving");
}
}

class Bike extends Vehicle {
move(): void {
console.log("Bike is cycling");
}
}

const car = new Car();
const bike = new Bike();

car.move(); // Output: Car is driving
bike.move(); // Output: Bike is cycling

In this example:

  • Vehicle is the base class, and Car and Bike are subclasses that extend Vehicle.
  • Each subclass provides its own implementation of the move() method, creating a polymorphic behavior.

Abstract Classes and Their Role in Inheritance

An abstract class is a class that cannot be instantiated directly. It can be extended by other classes, which must implement its abstract methods. Abstract classes allow you to define methods that must be implemented by subclasses, ensuring that certain functionality is always provided.

Example of Abstract Class

abstract class Shape {
abstract area(): number;
}

class Circle extends Shape {
radius: number;

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

area(): number {
return Math.PI * this.radius ** 2;
}
}

const circle = new Circle(5);
console.log(circle.area()); // Output: 78.53981633974483

In this example:

  • Shape is an abstract class with an abstract area() method.
  • Circle extends Shape and implements the area() method.

You cannot create an instance of the Shape class directly; it must be subclassed and the abstract methods implemented.


Constructor Inheritance and Initialization

When a subclass extends a parent class, the subclass inherits the constructor of the parent class. However, if the parent class’s constructor requires arguments, the subclass must call the parent constructor using the super() keyword.

Example of Constructor Inheritance

class Animal {
constructor(public name: string) {}

speak(): void {
console.log(`${this.name} speaks.`);
}
}

class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // Calls the parent class's constructor
}

speak(): void {
console.log(`${this.name} the ${this.breed} barks.`);
}
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Output: Buddy the Golden Retriever barks.

In this example:

  • The Dog class extends Animal and calls the Animal constructor using super(name).
  • The Dog class adds an additional property breed and overrides the speak() method.

Conclusion

Inheritance is a cornerstone of object-oriented programming, and TypeScript’s class system provides a powerful way to implement it. By using the extends keyword, TypeScript allows you to create hierarchies where child classes can inherit functionality from parent classes. You can also use abstract classes and interfaces to define reusable structures and enforce implementation contracts across your codebase.

With the ability to simulate multiple inheritance via interfaces and leveraging the super keyword for calling parent methods and constructors, TypeScript provides a robust model for building complex class hierarchies.

Access Modifiers: Public, Private, Protected

0
typscript course
typscript course

Table of Contents

  • Introduction
  • Understanding Access Modifiers in TypeScript
    • What are Access Modifiers?
    • Types of Access Modifiers
  • Public Access Modifier
    • Default Behavior
    • Example of Public Access Modifier
  • Private Access Modifier
    • How It Works
    • Example of Private Access Modifier
  • Protected Access Modifier
    • When to Use Protected
    • Example of Protected Access Modifier
  • Access Modifiers in Inheritance
    • Inherited Members and Access Modifiers
  • Conclusion

Introduction

TypeScript, as a statically typed superset of JavaScript, introduces several powerful features to enhance code clarity, maintainability, and scalability. One of the fundamental features is the ability to define access modifiers for class members. Access modifiers control the visibility and accessibility of class members (properties and methods) from outside the class. These modifiers help to implement encapsulation — one of the core principles of object-oriented programming (OOP).

In this article, we will explore the three main access modifiers in TypeScript: public, private, and protected. We will also discuss how these modifiers can be applied in real-world scenarios, along with examples to illustrate their usage.


Understanding Access Modifiers in TypeScript

What are Access Modifiers?

Access modifiers are keywords that define the visibility of class members. They determine whether properties and methods can be accessed from outside the class or whether they are restricted to the class or its subclasses. TypeScript provides three main access modifiers:

  1. Public: Members are accessible from anywhere.
  2. Private: Members are only accessible within the class.
  3. Protected: Members are accessible within the class and its subclasses.

Access modifiers allow you to implement encapsulation, a key concept in object-oriented programming, which helps to protect the internal state of objects and restrict access to critical data.


Types of Access Modifiers

1. Public Access Modifier

The public access modifier is the default for all members in a TypeScript class. Public members can be accessed from anywhere, both inside and outside the class. This is useful when you want a property or method to be freely accessible by other parts of your program.

Default Behavior

In TypeScript, if no access modifier is specified for a member, it is automatically treated as public.

Example of Public Access Modifier

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

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

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

const car = new Car("Toyota", "Corolla");
console.log(car.make); // Accessible from outside the class
console.log(car.model); // Accessible from outside the class
car.displayDetails(); // Accessible from outside the class

In this example, both make, model, and the displayDetails() method are marked as public. These can be accessed directly from the car object.


2. Private Access Modifier

The private access modifier restricts access to members, making them accessible only within the class that defines them. Private members cannot be accessed or modified from outside the class, even by instances of the class.

This is useful when you want to hide the internal details of a class and only expose a public API for interaction.

How It Works

Private members are not visible to outside code, even if you try to access them through an object instance.

Example of Private Access Modifier

class BankAccount {
private balance: number;

constructor(initialBalance: number) {
this.balance = initialBalance;
}

public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
}
}

public getBalance(): number {
return this.balance;
}
}

const account = new BankAccount(1000);
account.deposit(500);
// account.balance = 2000; // Error: Property 'balance' is private and only accessible within class 'BankAccount'.
console.log(account.getBalance()); // Output: 1500

In this example, balance is a private member of the BankAccount class. You cannot directly access or modify balance from outside the class. The getBalance() method is provided to access the value of balance.

Attempting to access balance directly will result in a compile-time error.


3. Protected Access Modifier

The protected access modifier is similar to private, but with a key difference: protected members are accessible within the class and its subclasses (derived classes). This makes the protected modifier particularly useful when you want to share some functionality with subclasses but hide it from external code.

When to Use Protected

Use the protected modifier when you want to allow subclasses to have access to certain class members while keeping them hidden from outside code.

Example of Protected Access Modifier

class Animal {
protected name: string;

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

protected speak(): void {
console.log(`${this.name} makes a sound.`);
}
}

class Dog extends Animal {
constructor(name: string) {
super(name);
}

public bark(): void {
console.log(`${this.name} barks.`);
}
}

const dog = new Dog("Rex");
dog.bark(); // Output: Rex barks
// dog.speak(); // Error: Property 'speak' is protected and only accessible within class 'Animal' and its subclasses.

In this example:

  • name and speak() are protected in the Animal class.
  • Dog, which extends Animal, can access name and speak() because they are marked as protected.
  • However, we cannot access speak() directly from an instance of Dog or from outside the class hierarchy.

Access Modifiers in Inheritance

When a class inherits from another class, the access modifiers on the parent class members dictate how those members can be accessed in the child class.

Inherited Members and Access Modifiers

  • Public members of the parent class can be accessed directly from the child class or outside code.
  • Private members are not accessible in the child class.
  • Protected members can be accessed within the child class but not outside of it.

Example: Inheritance with Access Modifiers

class Vehicle {
public model: string;
private year: number;
protected speed: number;

constructor(model: string, year: number, speed: number) {
this.model = model;
this.year = year;
this.speed = speed;
}

public accelerate(): void {
this.speed += 10;
}

private changeYear(newYear: number): void {
this.year = newYear;
}

protected getSpeed(): number {
return this.speed;
}
}

class Car extends Vehicle {
constructor(model: string, year: number, speed: number) {
super(model, year, speed);
}

public displaySpeed(): void {
console.log(`The car is moving at ${this.getSpeed()} km/h.`);
}
}

const car = new Car("Honda", 2021, 100);
console.log(car.model); // Accessible (public)
car.accelerate(); // Works because accelerate is public
// car.year = 2022; // Error: Property 'year' is private and only accessible within class 'Vehicle'.
// car.getSpeed(); // Error: Property 'getSpeed' is protected and only accessible within class 'Vehicle' and its subclasses.

In this example:

  • model is public and can be accessed both inside and outside the class.
  • year is private and cannot be accessed or modified directly by the Car class or its instances.
  • speed is protected and is accessible inside Car because it extends Vehicle.

Conclusion

Access modifiers are essential in TypeScript for enforcing encapsulation and ensuring that the internal state of a class is protected. By using public, private, and protected, you can control how class members are accessed and prevent unintended modifications.

  • Public members are freely accessible from anywhere.
  • Private members are only accessible within the class.
  • Protected members are accessible within the class and its subclasses.

Using access modifiers effectively helps to build more maintainable and secure applications by restricting access to sensitive or critical data and functions. They are a powerful tool to manage class design and inheritance in TypeScript.


Classes in TypeScript: Constructors and Members

0
typscript course
typscript course

Table of Contents

  • Introduction
  • Understanding Classes in TypeScript
    • What is a Class?
    • Defining a Basic Class
  • Class Members: Properties and Methods
    • Defining Properties
    • Defining Methods
    • Access Modifiers: Public, Private, and Protected
  • The Constructor Method in TypeScript
    • What is a Constructor?
    • Using Constructors for Initialization
    • Constructor Overloading
  • Static Members in Classes
  • Inheritance in TypeScript Classes
  • Practical Examples of Classes
  • Conclusion

Introduction

In TypeScript, classes are a blueprint for creating objects with shared properties and methods. They provide a way to model real-world entities in code and are a core concept in object-oriented programming (OOP). TypeScript enhances JavaScript’s class capabilities with strong typing, access modifiers, and other features, making it a powerful tool for building scalable and maintainable applications.

In this article, we’ll dive deep into classes in TypeScript, focusing on constructors, members (properties and methods), and other key aspects of class design.


Understanding Classes in TypeScript

What is a Class?

A class is a blueprint for creating objects. It defines the properties and methods that objects created from the class will have. Classes are a central feature of object-oriented programming (OOP) and are used to group data and functions together.

In TypeScript, the class syntax is similar to ES6 JavaScript classes, but with the added benefit of type checking.

Defining a Basic Class

Here’s how you define a simple class in TypeScript:

class Person {
name: string;
age: number;

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

greet() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
}

const person = new Person("John", 30);
person.greet(); // Output: Hello, my name is John and I am 30 years old.

In this example:

  • We define a Person class with two properties: name and age.
  • We define a constructor to initialize these properties.
  • We define a method greet() that prints a message.

Class Members: Properties and Methods

Classes in TypeScript can have various members, including properties (data) and methods (functions). These members can be public, private, or protected, depending on their access level.

Defining Properties

In TypeScript, properties in a class are defined just like variables, but with explicit types. For example:

class Person {
name: string;
age: number;

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

In this example, name is a string and age is a number.

You can also define optional properties using the ? syntax:

class Person {
name: string;
age: number;
nickname?: string; // Optional property

constructor(name: string, age: number, nickname?: string) {
this.name = name;
this.age = age;
if (nickname) {
this.nickname = nickname;
}
}
}

Defining Methods

Methods inside a class are defined like functions, and they operate on the class’s properties. Here’s an example:

class Person {
name: string;
age: number;

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

introduce(): void {
console.log(`Hi, I'm ${this.name} and I am ${this.age} years old.`);
}
}

const person = new Person("Alice", 25);
person.introduce(); // Output: Hi, I'm Alice and I am 25 years old.

Access Modifiers: Public, Private, and Protected

TypeScript provides three access modifiers to control the visibility of properties and methods:

  • public: The member is accessible from anywhere (default).
  • private: The member is only accessible within the class.
  • protected: The member is accessible within the class and its subclasses.

Example: Using Access Modifiers

class Person {
public name: string;
private age: number;
protected address: string;

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

public greet(): void {
console.log(`Hello, my name is ${this.name}.`);
}

private calculateAgeInMonths(): number {
return this.age * 12;
}

protected updateAddress(newAddress: string): void {
this.address = newAddress;
}
}

In this example:

  • name is public and can be accessed from anywhere.
  • age is private and can only be accessed within the Person class.
  • address is protected and can be accessed within the Person class and any derived classes.

The Constructor Method in TypeScript

The constructor is a special method that is called when a class is instantiated (i.e., when an object is created from the class). It is used to initialize the object’s properties.

What is a Constructor?

The constructor method is declared with the constructor keyword, followed by parameters and the this keyword to assign values to the object’s properties.

Example: Basic Constructor

class Person {
name: string;
age: number;

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

const person = new Person("John", 30);
console.log(person.name); // Output: John
console.log(person.age); // Output: 30

Using Constructors for Initialization

You can use the constructor to initialize properties when creating an object. If a property is required to be initialized on object creation, it can be added as a constructor parameter.

Example: Constructor with Optional Parameters

class Person {
name: string;
age: number;
city?: string;

constructor(name: string, age: number, city?: string) {
this.name = name;
this.age = age;
if (city) {
this.city = city;
}
}
}

const person = new Person("Jane", 28, "New York");
console.log(person.city); // Output: New York

Constructor Overloading

TypeScript doesn’t support traditional constructor overloading as seen in other languages like Java or C#. However, you can mimic constructor overloading by using optional parameters or union types.

Example: Constructor Overloading with Optional Parameters

class Person {
name: string;
age: number;

constructor(name: string, age: number);
constructor(name: string);
constructor(name: string, age?: number) {
this.name = name;
this.age = age ?? 0; // Default value if age is not provided
}

introduce() {
console.log(`Hi, I am ${this.name} and I am ${this.age} years old.`);
}
}

const person1 = new Person("John", 30);
person1.introduce(); // Output: Hi, I am John and I am 30 years old.

const person2 = new Person("Alice");
person2.introduce(); // Output: Hi, I am Alice and I am 0 years old.

Static Members in Classes

Static members belong to the class itself, rather than instances of the class. This means they are shared across all instances of the class.

Example: Using Static Members

class Calculator {
static pi: number = 3.14159;

static calculateArea(radius: number): number {
return Calculator.pi * radius * radius;
}
}

console.log(Calculator.pi); // Output: 3.14159
console.log(Calculator.calculateArea(5)); // Output: 78.53975

In this example, the pi property and calculateArea() method are static and are accessed directly from the class.


Inheritance in TypeScript Classes

TypeScript supports inheritance, allowing one class to inherit properties and methods from another.

Example: Inheritance

class Animal {
name: string;

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

speak() {
console.log(`${this.name} makes a sound.`);
}
}

class Dog extends Animal {
constructor(name: string) {
super(name); // Calling the parent class constructor
}

speak() {
console.log(`${this.name} barks.`);
}
}

const dog = new Dog("Rex");
dog.speak(); // Output: Rex barks.

In this example:

  • Dog extends Animal, inheriting its properties and methods.
  • speak() is overridden in the Dog class to provide a more specific implementation.

Practical Examples of Classes

Example 1: Building a Simple Bank Account

class BankAccount {
private balance: number;

constructor(initialBalance: number) {
this.balance = initialBalance;
}

deposit(amount: number): void {
this.balance += amount;
}

withdraw(amount: number): void {
if (amount <= this.balance) {
this.balance -= amount;
} else {
console.log("Insufficient funds.");
}
}

getBalance(): number {
return this.balance;
}
}

const account = new BankAccount(1000);
account.deposit(500);
account.withdraw(200);
console.log(account.getBalance()); // Output: 1300

In this example, the BankAccount class has methods to deposit and withdraw money, while keeping the balance property private.


Conclusion

Classes are a fundamental building block in TypeScript, providing a structured and type-safe way to create objects. In this article, we’ve explored the key concepts of constructors and members in TypeScript classes, including how to define properties, methods, and static members. We’ve also covered important topics such as inheritance and access modifiers.

Understanding how to use classes effectively allows you to write more organized, maintainable, and robust TypeScript code.