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.
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 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 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!"
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.
new
keyword when creating an object.Inside the constructor and methods, you can access the object’s properties using the this
keyword. This refers to the current object instance.
TypeScript classes allow you to control the accessibility of properties and methods using access modifiers:
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 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."
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 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 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 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 allow you to create classes that can work with different data types. This promotes type safety and code reusability.
class ArrayUtil {
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([1, 2, 3]);
console.log(numbers.getFirst()); // Output: 1
console.log(numbers.getLast()); // Output: 3
const strings = new ArrayUtil(["Hello", "World"]);
console.log(strings.getFirst()); // Output: "Hello"
console.log(strings.getLast()); // Output: "World"
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.");
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.");
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 !❤️