Classes#
TypeScript adds powerful type checking to JavaScript classes, along with features like access modifiers, abstract classes, and interfaces.
Basic Class Syntax#
class Person {
// Properties
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.`;
}
}
const person = new Person("Alice", 25);
console.log(person.greet());
Property Initialization#
// Traditional initialization
class User {
id: number;
name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
}
}
// Shorthand with parameter properties
class User2 {
constructor(
public id: number,
public name: string
) {}
}
// Both classes work the same way
const user1 = new User(1, "Alice");
const user2 = new User2(1, "Alice");
Access Modifiers#
Public (default)#
class Car {
public brand: string;
constructor(brand: string) {
this.brand = brand;
}
public drive(): void {
console.log(`Driving ${this.brand}`);
}
}
const car = new Car("Toyota");
console.log(car.brand); // ✅ Accessible
car.drive(); // ✅ Accessible
Private#
class BankAccount {
private balance: number = 0;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
public deposit(amount: number): void {
this.balance += amount;
}
public getBalance(): number {
return this.balance;
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // ✅ 1500
// console.log(account.balance); // ❌ Error: Property 'balance' is private
Protected#
class Animal {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
private breed: string;
constructor(name: string, breed: string) {
super(name);
this.breed = breed;
}
public getInfo(): string {
return `${this.name} is a ${this.breed}`; // ✅ Can access protected property
}
}
const dog = new Dog("Buddy", "Labrador");
console.log(dog.getInfo()); // ✅ "Buddy is a Labrador"
// console.log(dog.name); // ❌ Error: Property 'name' is protected
Readonly Properties#
class Configuration {
readonly apiKey: string;
readonly apiUrl: string;
timeout: number;
constructor(apiKey: string, apiUrl: string, timeout: number) {
this.apiKey = apiKey;
this.apiUrl = apiUrl;
this.timeout = timeout;
}
updateTimeout(newTimeout: number): void {
this.timeout = newTimeout; // ✅ OK
// this.apiKey = "new-key"; // ❌ Error: Cannot assign to 'apiKey'
}
}
Getters and Setters#
class User {
private _email: string = "";
get email(): string {
return this._email;
}
set email(value: string) {
if (!value.includes("@")) {
throw new Error("Invalid email format");
}
this._email = value;
}
// Computed property
private _firstName: string = "";
private _lastName: string = "";
get fullName(): string {
return `${this._firstName} ${this._lastName}`;
}
set fullName(value: string) {
const [first, last] = value.split(" ");
this._firstName = first;
this._lastName = last;
}
}
const user = new User();
user.email = "user@example.com"; // ✅ OK
console.log(user.email);
// user.email = "invalid"; // ❌ Throws error
user.fullName = "John Doe";
console.log(user.fullName); // "John Doe"
Static Members#
class MathUtils {
static PI: number = 3.14159;
static calculateCircleArea(radius: number): number {
return this.PI * radius * radius;
}
static max(...numbers: number[]): number {
return Math.max(...numbers);
}
}
// Access without instantiation
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircleArea(5)); // 78.53975
console.log(MathUtils.max(1, 5, 3, 9, 2)); // 9
Inheritance#
class Animal {
constructor(public name: string) {}
move(distance: number): void {
console.log(`${this.name} moved ${distance} meters.`);
}
}
class Dog extends Animal {
constructor(name: string, public breed: string) {
super(name); // Call parent constructor
}
bark(): void {
console.log("Woof! Woof!");
}
// Override parent method
move(distance: number): void {
console.log("Running...");
super.move(distance); // Call parent method
}
}
const dog = new Dog("Buddy", "Labrador");
dog.bark(); // "Woof! Woof!"
dog.move(10); // "Running..." then "Buddy moved 10 meters."
Abstract Classes#
abstract class Shape {
abstract getArea(): number;
abstract getPerimeter(): number;
// Concrete method
describe(): string {
return `Area: ${this.getArea()}, Perimeter: ${this.getPerimeter()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
const circle = new Circle(5);
console.log(circle.describe());
const rectangle = new Rectangle(4, 6);
console.log(rectangle.describe());
// const shape = new Shape(); // ❌ Error: Cannot create an instance of an abstract class
Implementing Interfaces#
interface Printable {
print(): void;
}
interface Saveable {
save(): Promise<void>;
}
class Document implements Printable, Saveable {
constructor(private content: string) {}
print(): void {
console.log(this.content);
}
async save(): Promise<void> {
// Simulate async save
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Document saved");
}
}
const doc = new Document("Hello, World!");
doc.print();
await doc.save();
Generic Classes#
class Box<T> {
private item: T | null = null;
setItem(item: T): void {
this.item = item;
}
getItem(): T | null {
return this.item;
}
hasItem(): boolean {
return this.item !== null;
}
}
const numberBox = new Box<number>();
numberBox.setItem(42);
console.log(numberBox.getItem()); // 42
const stringBox = new Box<string>();
stringBox.setItem("Hello");
console.log(stringBox.getItem()); // "Hello"
// Generic class with constraints
class Repository<T extends { id: number }> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
getAll(): T[] {
return this.items;
}
}
interface User {
id: number;
name: string;
}
const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: "Alice" });
userRepo.add({ id: 2, name: "Bob" });
console.log(userRepo.findById(1));
Method Overloading#
class Calculator {
// Overload signatures
add(a: number, b: number): number;
add(a: string, b: string): string;
add(a: number[], b: number[]): number[];
// Implementation
add(a: any, b: any): any {
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
if (typeof a === "string" && typeof b === "string") {
return a + b;
}
if (Array.isArray(a) && Array.isArray(b)) {
return [...a, ...b];
}
}
}
const calc = new Calculator();
console.log(calc.add(1, 2)); // 3
console.log(calc.add("Hello, ", "World!")); // "Hello, World!"
console.log(calc.add([1, 2], [3, 4])); // [1, 2, 3, 4]
Practical Examples#
Singleton Pattern#
class Database {
private static instance: Database;
private connected: boolean = false;
private constructor() {
// Private constructor prevents direct instantiation
}
static getInstance(): Database {
if (!Database.instance) {
Database.instance = new Database();
}
return Database.instance;
}
connect(): void {
if (!this.connected) {
console.log("Connecting to database...");
this.connected = true;
}
}
disconnect(): void {
if (this.connected) {
console.log("Disconnecting from database...");
this.connected = false;
}
}
}
// const db = new Database(); // ❌ Error: Constructor is private
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2); // true - same instance
Repository Pattern#
abstract class BaseRepository<T> {
protected items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return [...this.items];
}
abstract findById(id: string | number): T | undefined;
abstract delete(id: string | number): boolean;
}
interface User {
id: number;
name: string;
email: string;
}
class UserRepository extends BaseRepository<User> {
findById(id: number): User | undefined {
return this.items.find(user => user.id === id);
}
delete(id: number): boolean {
const index = this.items.findIndex(user => user.id === id);
if (index !== -1) {
this.items.splice(index, 1);
return true;
}
return false;
}
findByEmail(email: string): User | undefined {
return this.items.find(user => user.email === email);
}
}
const userRepo = new UserRepository();
userRepo.add({ id: 1, name: "Alice", email: "alice@example.com" });
userRepo.add({ id: 2, name: "Bob", email: "bob@example.com" });
Event Emitter#
type EventHandler<T = any> = (data: T) => void;
class EventEmitter {
private events: Map<string, EventHandler[]> = new Map();
on(event: string, handler: EventHandler): () => void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(handler);
// Return unsubscribe function
return () => this.off(event, handler);
}
off(event: string, handler: EventHandler): void {
const handlers = this.events.get(event);
if (handlers) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
emit(event: string, data?: any): void {
const handlers = this.events.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
once(event: string, handler: EventHandler): void {
const onceHandler: EventHandler = (data) => {
handler(data);
this.off(event, onceHandler);
};
this.on(event, onceHandler);
}
}
// Usage
const emitter = new EventEmitter();
const unsubscribe = emitter.on("userLogin", (data) => {
console.log(`User logged in:`, data);
});
emitter.emit("userLogin", { userId: "123", timestamp: Date.now() });
unsubscribe(); // Stop listening
Builder Pattern#
class HttpRequest {
private url: string = "";
private method: string = "GET";
private headers: Record<string, string> = {};
private body: any = null;
setUrl(url: string): this {
this.url = url;
return this;
}
setMethod(method: string): this {
this.method = method;
return this;
}
addHeader(key: string, value: string): this {
this.headers[key] = value;
return this;
}
setBody(body: any): this {
this.body = body;
return this;
}
async send(): Promise<Response> {
return fetch(this.url, {
method: this.method,
headers: this.headers,
body: this.body ? JSON.stringify(this.body) : undefined
});
}
}
// Usage
const response = await new HttpRequest()
.setUrl("https://api.example.com/users")
.setMethod("POST")
.addHeader("Content-Type", "application/json")
.addHeader("Authorization", "Bearer token")
.setBody({ name: "Alice", email: "alice@example.com" })
.send();
Best Practices#
-
✅ Use parameter properties for concise code
constructor(public name: string, private age: number) {} -
✅ Prefer composition over inheritance
class Car { constructor(private engine: Engine) {} } -
✅ Use access modifiers appropriately
class User { private password: string; // Hide sensitive data public name: string; // Public API } -
✅ Make classes immutable with readonly
class Config { constructor(readonly apiKey: string) {} } -
✅ Use abstract classes for shared behavior
abstract class BaseService { abstract getData(): Promise<any>; } -
❌ Avoid deep inheritance hierarchies
// ❌ Too deep class A {} class B extends A {} class C extends B {} class D extends C {}
Next Steps#
- Learn about Generics for reusable classes
- Explore Interfaces & Types to implement
- Master Utility Types for advanced patterns