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.