TypeScript Classes and Objects
TypeScript enhances the standard JavaScript class syntax by integrating a robust type system, making Object-Oriented Programming (OOP) more predictable, scalable, and easier to maintain. By providing explicit type annotations and advanced access controls, TypeScript allows developers to build complex software architectures with a high degree of confidence.
At its core, a class serves as a blueprint for creating objects. In TypeScript, when you define a class, you must specify the types for its properties. This ensures that every instance of the class adheres to a strict structure. The constructor is a special method used to initialize new instances, and TypeScript's type checker validates that the arguments passed during instantiation match the expected types and count.
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): string {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}
const person: Person = new Person("Alice", 30);
console.log(person.greet()); // Hello, my name is Alice and I am 30 years old.
When using the new keyword to instantiate an object, TypeScript performs compile-time checks to prevent common runtime errors, such as passing a string where a number is required.
Access Modifiers and Immutability
One of the most powerful features of TypeScript classes is the ability to control the visibility of class members through access modifiers. This is essential for implementing encapsulation, ensuring that the internal state of an object is protected from unauthorized external interference.
public: The default modifier. Members marked aspublicare accessible from anywhere—inside the class, in subclasses, and by external instances.private: Members are only accessible within the class where they are defined. They are hidden from subclasses and external code.protected: Members are accessible within the class and by any subclasses that extend it, but remain inaccessible to external instances.
In addition to visibility, TypeScript offers the readonly modifier. A readonly property can only be assigned a value during its declaration or within the constructor. Once initialized, its value cannot be changed, which is ideal for constants or unique identifiers.
class Account {
readonly id: number;
private balance: number;
constructor(id: number, balance: number) {
this.id = id;
this.balance = balance;
}
deposit(amount: number): void {
this.balance += amount;
}
}
Parameter Properties: A Syntactic Shortcut
To reduce boilerplate code, TypeScript provides parameter properties. Instead of declaring a property and then assigning it in the constructor, you can simply add an access modifier directly to the constructor arguments. This automatically handles both the declaration and the assignment.
class User {
// This single line declares and initializes 'username' and 'password'
constructor(public username: string, private password: string) {}
}
Accessors: Getters and Setters
Sometimes, you need to execute logic when a property is accessed or modified. TypeScript provides accessors via the get and set keywords. This allows you to treat a method like a property while maintaining control over data validation or transformation.
class Employee {
private _salary: number = 0;
get salary(): number {
return this._salary;
}
set salary(value: number) {
if (value < 0) {
throw new Error("Salary cannot be negative.");
}
this._salary = value;
}
}
Static Members
While most class members belong to an instance of the class, static members belong to the class itself. Static properties and methods are used for utility functions or global state that should be shared across all instances, rather than being unique to each one.
class Counter {
static count: number = 0;
static increment(): void {
Counter.count++;
}
}
Counter.increment();
console.log(Counter.count); // 1
Inheritance and the super Keyword
Inheritance allows a class (the subclass) to inherit properties and methods from another class (the superclass), promoting code reuse. Using the extends keyword, a subclass can build upon the foundation of a parent class.
When a subclass defines its own constructor, it must call super() before accessing this. This ensures that the parent class is properly initialized.
class Animal {
constructor(protected name: string) {}
describe(): void {
console.log(`I am an animal named ${this.name}`);
}
}
class Dog extends Animal {
bark(): void {
console.log(`${this.name} is barking!`);
}
}
const myDog = new Dog("Buddy");
myDog.describe(); // I am an animal named Buddy
myDog.bark(); // Buddy is barking!
In this example, because name was declared as protected in the Animal class, the Dog subclass can access it directly.
Abstract Classes and Interfaces
For high-level architectural design, TypeScript provides two critical tools: abstract classes and interfaces.
Abstract Classes
An abstract class acts as a partial blueprint. It is declared using the abstract keyword and cannot be instantiated directly. It is designed to be extended by other classes. Abstract classes can contain both implemented methods and abstract methods—methods that have no body and must be implemented by any non-abstract subclass.
abstract class Shape {
abstract area(): number;
describe(): void {
console.log(`The area is ${this.area().toFixed(2)}`);
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(2);
circle.describe(); // The area is 12.57
Implementing Interfaces
While abstract classes define "what an object is," interfaces define "what an object can do." By using the implements keyword, a class commits to following a specific structural contract, ensuring it contains all the required properties and methods defined by the interface.
interface Serializable {
serialize(): string;
}
class Product implements Serializable {
constructor(public name: string, public price: number) {}
serialize(): string {
return JSON.stringify(this);
}
}
Summary
TypeScript's class system bridges the gap between the flexibility of JavaScript and the rigor required for enterprise-level development. By mastering access modifiers, inheritance, abstract classes, and interfaces, developers can write code that is not only type-safe but also highly modular and easy to extend. For cleaner code, leverage parameter properties where possible, and use private or protected to maintain strict encapsulation of your object's internal state.