Typescript classes

TypeScript, building upon JavaScript's prototype-based inheritance, offers a familiar and powerful concept: classes. Classes provide a blueprint for creating objects, encapsulating data (properties) and behavior (methods) within a single unit. This chapter delves into the world of TypeScript classes, covering everything from the basics to advanced concepts.

Basic Class Definition

Creating a Class

You define a class using the class keyword followed by the class name and curly braces {} to enclose the class body:

				
					class Person {
  // Class members (properties and methods) go here
}

				
			

Properties

Properties are variables associated with an object created from a class. They represent the data or attributes of an object.

				
					class Person {
  name: string; // Property with type annotation
  age: number;

  constructor(name: string, age: number) { // Constructor to initialize properties
    this.name = name;
    this.age = age;
  }
}

const person1 = new Person("Alice", 30);  // Creating an object (instance) of Person
console.log(person1.name); // Output: "Alice"
console.log(person1.age);  // Output: 30

				
			

Methods

Methods are functions defined within a class. They represent the behavior or actions that objects can perform. Methods typically operate on the data (properties) of the object.

				
					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 + "!");
  }
}

const person1 = new Person("Alice", 30);
person1.greet(); // Output: "Hello, my name is Alice!"

				
			

Constructors

Object Initialization

The constructor is a special method that is invoked when a new object (instance) is created from the class. It’s used to initialize the object’s properties with starting values.

  • The constructor must have the same name as the class.
  • It’s typically called with the new keyword when creating an object.

Accessing Properties with this

Inside the constructor and methods, you can access the object’s properties using the this keyword. This refers to the current object instance.

Access Modifiers

Public, Private, and Protected

TypeScript classes allow you to control the accessibility of properties and methods using access modifiers:

  • Public: Accessible from anywhere, both inside and outside the class (default).
  • Private: Accessible only from within the class where they are defined.
  • Protected: Accessible from within the class and its subclasses.
				
					class Person {
  private name: string; // Private property
  protected age: number;  // Protected property

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

  public greet() {
    console.log("Hello, my name is " + this.name + "!");
  }

  // Subclass can access protected age
  public getAgeInYearsFromNow(targetYear: number) {
    return targetYear - this.age;
  }
}

const person1 = new Person("Alice", 30);
// person1.name; // Error: name is private
person1.greet(); // Output: "Hello, my name is Alice!"

// Subclass example (assuming a separate Subclass file)
class Employee extends Person {
  constructor(name: string, age: number, jobTitle: string) {
    super(name, age); // Call superclass constructor to initialize protected age
  }

  public getYearsUntilRetirement(retirementAge: number) {
    return this.getAgeInYearsFromNow(retirementAge); // Access protected age
  }
}

const employee1 = new Employee("Bob", 40, "Software Engineer");
console.log(employee1.getYearsUntilRetirement(65)); // Output: 25 (assuming logic works)

				
			

Inheritance

Code Reusability

Inheritance allows you to create new classes (subclasses) that inherit properties and methods from an existing class (superclass). This promotes code reusability and reduces code duplication.

				
					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 + "!");
  }
}

class Employee extends Person { // Employee inherits from Person
  jobTitle: string;

  constructor(name: string, age: number, jobTitle: string) {
    super(name, age); // Call superclass constructor to initialize inherited properties
    this.jobTitle = jobTitle;
  }

  greetWithJobTitle() {
    console.log("Hello, I'm " + this.name + ", a " + this.jobTitle + ".");
  }
}

const employee1 = new Employee("Bob", 40, "Software Engineer");
employee1.greet();       // Output: "Hello, my name is Bob!" (inherited)
employee1.greetWithJobTitle(); // Output: "Hello, I'm Bob, a Software Engineer."

				
			

Method Overriding

Redefining Inherited Behavior

Subclasses can override inherited methods from the superclass to provide their own implementation. This allows for specialization of behavior in subclasses.

				
					class Person {
  greet() {
    console.log("Hello from Person!");
  }
}

class Student extends Person {
  greet() {
    console.log("Hello from Student! (Overridden)");
  }
}

const person1 = new Person();
person1.greet(); // Output: "Hello from Person!"

const student1 = new Student();
student1.greet(); // Output: "Hello from Student! (Overridden)"

				
			

Polymorphism

“One Interface, Multiple Forms”

Polymorphism allows objects of different classes (subclasses) to respond to the same method call (often through inheritance) in different ways. This provides flexibility and promotes loosely coupled code.

				
					function introduceSomeone(person: Person) {
  person.greet();
}

const person1 = new Person("Alice", 30);
const student1 = new Student("Bob", 25);

introduceSomeone(person1); // Output: "Hello from Person!"
introduceSomeone(student1); // Output: "Hello from Student! (Overridden)"

				
			

Abstract Classes

Blueprints with Incomplete Implementations

Abstract classes cannot be directly instantiated. They serve as blueprints to define a contract (properties and methods) that subclasses must adhere to. Subclasses must implement abstract methods (methods without implementation in the abstract class).

				
					abstract class Shape {
  abstract getArea(): number; // Abstract method, subclasses must implement

  toString() {
    return "This is a shape.";
  }
}

class Square extends Shape {
  sideLength: number;

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

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

const square1 = new Square(5);
console.log(square1.getArea()); // Output: 25
// const shape1 = new Shape(); // Error: Abstract classes cannot be instantiated directly

				
			

Interfaces

Contracts for Object Structure and Behavior

Interfaces define contracts that specify the structure (properties) and behavior (methods) that a class or object must adhere to. They are similar to abstract classes but cannot contain implementation details.

				
					interface Greetable {
  greet(): void;
}

class Person implements Greetable {
  name: string;

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

  greet() {
    console.log("Hello, my name is " + this.name + "!");
  }
}

const person1 = new Person("Alice");
person1.greet(); // Output: "Hello, my name is Alice!"

				
			

Generics in Classes

Flexible Class Design

Generics allow you to create classes that can work with different data types. This promotes type safety and code reusability.

				
					class ArrayUtil<T> {
  private data: T[];

  constructor(data: T[]) {
    this.data = data;
  }

  getFirst(): T {
    return this.data[0];
  }

  getLast(): T {
    return this.data[this.data.length - 1];
  }
}

const numbers = new ArrayUtil<number>([1, 2, 3]);
console.log(numbers.getFirst());   // Output: 1
console.log(numbers.getLast());    // Output: 3

const strings = new ArrayUtil<string>(["Hello", "World"]);
console.log(strings.getFirst());  // Output: "Hello"
console.log(strings.getLast());   // Output: "World"

				
			

Mixins

Sharing Functionality Across Classes

Mixins are a concept (not a built-in feature) that allows you to share common functionality across unrelated classes. They are typically implemented using interfaces or utility classes.

				
					interface Logger {
  log(message: string): void;
}

class Database implements Logger {
  log(message: string): void {
    console.log("Database Log:", message);
  }

  // Other database methods...
}

class UserService implements Logger {
  log(message: string): void {
    console.log("User Service Log:", message);
  }

  // Other user service methods...
}

const db = new Database();
const userService = new UserService();

db.log("Connected to database.");
userService.log("User login successful.");

				
			

Advanced Class Patterns

Dependency Injection

Dependency injection (DI) is a technique where you provide a class with its dependencies (other objects it needs to function) through its constructor or methods. This promotes loose coupling and testability.

				
					class EmailService {
  constructor(private smtpClient: SmtpClient) {} // Dependency injection

  send(to: string, subject: string, body: string) {
    this.smtpClient.send(to, subject, body);
  }
}

class SmtpClient {
  // Methods for sending emails...
}

const smtpClient = new SmtpClient();
const emailService = new EmailService(smtpClient);

emailService.send("alice@example.com", "Hello", "This is a test email.");

				
			

Additional Considerations

  • Static Members: Classes can have static properties and methods that are accessible without creating an object instance.
  • Decorators: Decorators are a powerful feature in TypeScript that allows you to modify the behavior of classes, properties, and methods at runtime.
  • Namespace Pattern: Namespaces can be used to organize related classes and modules within larger projects.

Best Practices for TypeScript Classes

  • Use clear and descriptive class names that reflect their purpose.
  • Employ access modifiers (public, private, protected) to control the accessibility of properties and methods.
  • Leverage inheritance and polymorphism for code reusability and flexibility.
  • Consider using abstract classes and interfaces to define contracts for your classes.
  • Explore generics to create classes that work with various data types.
  • Apply dependency injection to promote loose coupling and testability.
  • Test your classes thoroughly to ensure they behave as expected.

TypeScript classes provide a powerful and flexible way to structure your object-oriented code. By understanding the concepts explained in this chapter, you can write well-organized, maintainable, and type-safe TypeScript applications. Remember to choose the appropriate class features based on your specific needs and maintain a balance between complexity and readability. Happy coding !❤️

Table of Contents