TypeScript

Classes in TypeScript

28 min Lesson 13 of 40

Object-Oriented Programming with TypeScript Classes

TypeScript enhances JavaScript classes with powerful type features, access modifiers, abstract classes, and more. Understanding TypeScript classes is essential for building robust object-oriented applications.

Basic Class Syntax

TypeScript classes extend JavaScript ES6 class syntax with type annotations and additional features:

// Basic class definition class Person { // Properties with type annotations name: string; age: number; // Constructor constructor(name: string, age: number) { this.name = name; this.age = age; } // Method greet(): string { return `Hello, I'm ${this.name} and I'm ${this.age} years old.`; } // Method with parameters haveBirthday(): void { this.age++; console.log(`Happy birthday! Now I'm ${this.age}.`); } } // Creating instances const person1 = new Person("Alice", 30); const person2 = new Person("Bob", 25); console.log(person1.greet()); // "Hello, I'm Alice and I'm 30 years old." person2.haveBirthday(); // "Happy birthday! Now I'm 26."
Note: In TypeScript, you must explicitly declare class properties before using them in the constructor. This is different from JavaScript where properties can be added dynamically.

Access Modifiers

TypeScript provides three access modifiers to control property and method visibility:

class BankAccount { // Public: accessible from anywhere (default) public accountHolder: string; // Private: only accessible within the class private balance: number; // Protected: accessible within the class and subclasses protected accountNumber: string; constructor(holder: string, initialBalance: number) { this.accountHolder = holder; this.balance = initialBalance; this.accountNumber = this.generateAccountNumber(); } // Public method public deposit(amount: number): void { if (amount > 0) { this.balance += amount; console.log(`Deposited $${amount}. New balance: $${this.balance}`); } } // Public method public getBalance(): number { return this.balance; } // Private method private generateAccountNumber(): string { return `ACC-${Math.random().toString(36).substr(2, 9).toUpperCase()}`; } // Protected method protected logTransaction(type: string, amount: number): void { console.log(`[${type}] $${amount} - Account: ${this.accountNumber}`); } } const account = new BankAccount("Alice", 1000); account.deposit(500); // OK: public method console.log(account.accountHolder); // OK: public property console.log(account.getBalance()); // OK: public method // account.balance; // Error: private property // account.accountNumber; // Error: protected property // account.generateAccountNumber(); // Error: private method
Tip: Use private for implementation details that shouldn't be accessed outside the class, protected for members that subclasses need access to, and public (or omit the modifier) for the public API.

Parameter Properties

TypeScript provides a shorthand syntax for declaring and initializing properties in the constructor:

// Traditional way class User1 { name: string; email: string; age: number; constructor(name: string, email: string, age: number) { this.name = name; this.email = email; this.age = age; } } // Parameter properties shorthand class User2 { constructor( public name: string, public email: string, private age: number ) { // Properties are automatically declared and initialized } getAge(): number { return this.age; } } const user = new User2("Alice", "alice@example.com", 30); console.log(user.name); // "Alice" console.log(user.email); // "alice@example.com" // console.log(user.age); // Error: private property console.log(user.getAge()); // 30 // Mix parameter properties with regular initialization class Product { private createdAt: Date; constructor( public id: number, public name: string, private price: number ) { this.createdAt = new Date(); } getPrice(): number { return this.price; } getCreatedAt(): Date { return this.createdAt; } }

Readonly Properties

Use the readonly modifier to create properties that can only be assigned during initialization:

class Circle { readonly PI: number = 3.14159; readonly radius: number; constructor(radius: number) { this.radius = radius; } getArea(): number { return this.PI * this.radius ** 2; } // This would cause an error: // setRadius(newRadius: number): void { // this.radius = newRadius; // Error: readonly property // } } const circle = new Circle(5); console.log(circle.getArea()); // 78.53975 // circle.radius = 10; // Error: readonly property // circle.PI = 3.14; // Error: readonly property // Combine readonly with parameter properties class ImmutablePoint { constructor( public readonly x: number, public readonly y: number ) {} distanceFromOrigin(): number { return Math.sqrt(this.x ** 2 + this.y ** 2); } } const point = new ImmutablePoint(3, 4); console.log(point.x); // 3 console.log(point.distanceFromOrigin()); // 5 // point.x = 10; // Error: readonly property

Getters and Setters

TypeScript supports accessor methods for controlled property access:

class Temperature { private _celsius: number = 0; get celsius(): number { return this._celsius; } set celsius(value: number) { if (value < -273.15) { throw new Error("Temperature cannot be below absolute zero"); } this._celsius = value; } get fahrenheit(): number { return (this._celsius * 9/5) + 32; } set fahrenheit(value: number) { this.celsius = (value - 32) * 5/9; } } const temp = new Temperature(); temp.celsius = 25; // Uses setter console.log(temp.celsius); // 25 (uses getter) console.log(temp.fahrenheit); // 77 (uses getter) temp.fahrenheit = 98.6; // Uses setter console.log(temp.celsius); // 37 (approximately) // temp.celsius = -300; // Error: Temperature cannot be below absolute zero // Read-only property using getter only class Counter { private _count: number = 0; get count(): number { return this._count; } increment(): void { this._count++; } decrement(): void { this._count--; } } const counter = new Counter(); counter.increment(); console.log(counter.count); // 1 // counter.count = 10; // Error: Cannot assign (no setter)
Warning: TypeScript getters must not have parameters, and setters must have exactly one parameter. The return type of a getter cannot be void, and the return type of a setter is always void.

Inheritance

TypeScript classes support single inheritance using the extends keyword:

// Base class class Animal { constructor( public name: string, protected age: number ) {} makeSound(): void { console.log("Some generic animal sound"); } getInfo(): string { return `${this.name} is ${this.age} years old`; } } // Derived class class Dog extends Animal { constructor( name: string, age: number, public breed: string ) { super(name, age); // Call parent constructor } // Override parent method makeSound(): void { console.log("Woof! Woof!"); } // Additional method fetch(): void { console.log(`${this.name} is fetching the ball!`); } // Override with additional logic getInfo(): string { return `${super.getInfo()} and is a ${this.breed}`; } } const dog = new Dog("Buddy", 3, "Golden Retriever"); dog.makeSound(); // "Woof! Woof!" dog.fetch(); // "Buddy is fetching the ball!" console.log(dog.getInfo()); // "Buddy is 3 years old and is a Golden Retriever" // Can use as base type const animal: Animal = dog; animal.makeSound(); // Still calls Dog's implementation: "Woof! Woof!"

Abstract Classes

Abstract classes serve as base classes that cannot be instantiated directly. They can contain abstract methods that must be implemented by derived classes:

// Abstract base class abstract class Shape { constructor(public color: string) {} // Abstract method - must be implemented by derived classes abstract getArea(): number; // Abstract method with different signature in derived classes abstract getPerimeter(): number; // Concrete method - inherited by derived classes describe(): string { return `A ${this.color} shape with area ${this.getArea()}`; } } // Cannot instantiate abstract class // const shape = new Shape("red"); // Error class Rectangle extends Shape { constructor( color: string, public width: number, public height: number ) { super(color); } getArea(): number { return this.width * this.height; } getPerimeter(): number { return 2 * (this.width + this.height); } } class Circle extends Shape { constructor( color: string, public radius: number ) { super(color); } getArea(): number { return Math.PI * this.radius ** 2; } getPerimeter(): number { return 2 * Math.PI * this.radius; } } const rect = new Rectangle("blue", 10, 5); console.log(rect.getArea()); // 50 console.log(rect.describe()); // "A blue shape with area 50" const circle = new Circle("red", 7); console.log(circle.getArea()); // 153.93804002589985 console.log(circle.getPerimeter()); // 43.982297150257104 // Abstract classes enable polymorphism const shapes: Shape[] = [rect, circle]; shapes.forEach(shape => { console.log(shape.describe()); });
Note: Abstract classes can have constructors, concrete methods, and properties. They're useful for sharing common functionality while enforcing that certain methods must be implemented by derived classes.

Implementing Interfaces

Classes can implement interfaces to ensure they provide specific functionality:

// Interface definition interface Printable { print(): void; } interface Serializable { serialize(): string; deserialize(data: string): void; } // Class implementing single interface class Document implements Printable { constructor(public content: string) {} print(): void { console.log(this.content); } } // Class implementing multiple interfaces class Report implements Printable, Serializable { constructor( public title: string, public content: string ) {} print(): void { console.log(`=== ${this.title} ===`); console.log(this.content); } serialize(): string { return JSON.stringify({ title: this.title, content: this.content }); } deserialize(data: string): void { const obj = JSON.parse(data); this.title = obj.title; this.content = obj.content; } } const report = new Report("Q4 Report", "Sales increased by 20%"); report.print(); // === Q4 Report === // Sales increased by 20% const serialized = report.serialize(); console.log(serialized); // {"title":"Q4 Report","content":"Sales increased by 20%"} const newReport = new Report("", ""); newReport.deserialize(serialized); newReport.print(); // === Q4 Report === // Sales increased by 20%

Static Members

Static properties and methods belong to the class itself, not instances:

class MathUtils { // Static property static PI: number = 3.14159; // Static method static calculateCircleArea(radius: number): number { return this.PI * radius ** 2; } // Static method accessing static property static degreesToRadians(degrees: number): number { return (degrees * this.PI) / 180; } // Instance method can access static members via class name static radiansToDegrees(radians: number): number { return (radians * 180) / MathUtils.PI; } } // Access static members via class name console.log(MathUtils.PI); // 3.14159 console.log(MathUtils.calculateCircleArea(5)); // 78.53975 console.log(MathUtils.degreesToRadians(180)); // 3.14159 // Static members not available on instances const utils = new MathUtils(); // console.log(utils.PI); // Error: static member // Practical example: Factory pattern class User { private static nextId: number = 1; constructor( public id: number, public name: string, public email: string ) {} static createUser(name: string, email: string): User { const user = new User(this.nextId++, name, email); return user; } } const user1 = User.createUser("Alice", "alice@example.com"); const user2 = User.createUser("Bob", "bob@example.com"); console.log(user1.id); // 1 console.log(user2.id); // 2
Exercise: Create an abstract Vehicle class with properties brand and model, and abstract methods start() and stop(). Implement two concrete classes: Car with a numberOfDoors property and Motorcycle with a hasSidecar property. Add a getDescription() method to the base class that uses the abstract methods. Create instances and test polymorphism by storing them in a Vehicle[] array.

Class Expressions

Like functions, classes can be defined as expressions:

// Named class expression const PersonClass = class Person { constructor(public name: string) {} greet(): string { return `Hello, I'm ${this.name}`; } }; const person = new PersonClass("Alice"); console.log(person.greet()); // "Hello, I'm Alice" // Anonymous class expression const GreeterClass = class { greet(name: string): string { return `Hello, ${name}!`; } }; const greeter = new GreeterClass(); console.log(greeter.greet("Bob")); // "Hello, Bob!" // Class expression as function return value function createGreeterClass(greeting: string) { return class { greet(name: string): string { return `${greeting}, ${name}!`; } }; } const SpanishGreeter = createGreeterClass("Hola"); const germanGreeter = new (createGreeterClass("Guten Tag"))(); const spanish = new SpanishGreeter(); console.log(spanish.greet("Carlos")); // "Hola, Carlos!" console.log(germanGreeter.greet("Hans")); // "Guten Tag, Hans!"

Summary

TypeScript classes provide a robust foundation for object-oriented programming with features like access modifiers, parameter properties, getters/setters, inheritance, abstract classes, and interface implementation. These features enable you to create well-structured, maintainable, and type-safe object-oriented code. Understanding classes is essential for building complex TypeScript applications and leveraging design patterns effectively.