From Zero to AI

Lesson 4.4: Classes in TypeScript

Duration: 60 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Create classes with typed properties and methods
  • Use constructors to initialize objects
  • Understand the difference between instance and static members
  • Implement getters and setters
  • Use parameter properties for cleaner code

What Are Classes?

Classes are blueprints for creating objects. They bundle data (properties) and behavior (methods) together:

// Without classes - separate data and functions
const userName = 'Alice';
const userAge = 30;

function greetUser(name: string) {
  console.log(`Hello, ${name}!`);
}

// With classes - data and behavior together
class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, ${this.name}!`);
  }
}

const user = new User('Alice', 30);
user.greet(); // "Hello, Alice!"

Basic Class Syntax

Declaring Properties

Properties must be declared with their types:

class Product {
  // Property declarations
  id: number;
  name: string;
  price: number;
  inStock: boolean;

  constructor(id: number, name: string, price: number) {
    this.id = id;
    this.name = name;
    this.price = price;
    this.inStock = true; // Default value
  }
}

const laptop = new Product(1, 'Laptop', 999);
console.log(laptop.name); // "Laptop"
console.log(laptop.inStock); // true

The Constructor

The constructor is called when you create a new instance with new:

class Rectangle {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
    console.log('Rectangle created!');
  }
}

const rect = new Rectangle(10, 20); // "Rectangle created!"

Methods

Methods are functions that belong to the class:

class Rectangle {
  width: number;
  height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  // Method to calculate area
  getArea(): number {
    return this.width * this.height;
  }

  // Method to calculate perimeter
  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }

  // Method to display info
  describe(): string {
    return `Rectangle: ${this.width}x${this.height}`;
  }
}

const rect = new Rectangle(10, 20);
console.log(rect.getArea()); // 200
console.log(rect.getPerimeter()); // 60
console.log(rect.describe()); // "Rectangle: 10x20"

Property Initialization

Default Values

Set default values directly on properties:

class Settings {
  theme: string = 'light';
  fontSize: number = 14;
  notifications: boolean = true;
  language: string = 'en';
}

const settings = new Settings();
console.log(settings.theme); // "light"

Optional Properties

Mark properties as optional with ?:

class UserProfile {
  username: string;
  email: string;
  bio?: string; // Optional
  website?: string; // Optional

  constructor(username: string, email: string) {
    this.username = username;
    this.email = email;
  }
}

const profile = new UserProfile('alice', 'alice@example.com');
console.log(profile.bio); // undefined
profile.bio = 'Software developer';
console.log(profile.bio); // "Software developer"

Readonly Properties

Properties marked readonly can only be set in the constructor:

class Person {
  readonly id: number;
  name: string;

  constructor(id: number, name: string) {
    this.id = id; // OK - in constructor
    this.name = name;
  }

  updateName(newName: string) {
    this.name = newName; // OK - name is not readonly
    // this.id = 999;     // Error! Cannot assign to 'id' because it is read-only
  }
}

const person = new Person(1, 'Alice');
person.name = 'Alicia'; // OK
// person.id = 2;         // Error! Cannot assign to 'id'

Parameter Properties

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

// Without parameter properties (verbose)
class User {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

// With parameter properties (concise)
class User {
  constructor(
    public name: string,
    public age: number
  ) {}
}

// Both create the same class!
const user = new User("Alice", 30);
console.log(user.name); // "Alice"

Parameter Properties with Modifiers

You can use public, private, protected, or readonly:

class BankAccount {
  constructor(
    public accountNumber: string,
    private balance: number,
    readonly createdAt: Date = new Date()
  ) {}

  getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount('123456', 1000);
console.log(account.accountNumber); // "123456"
console.log(account.getBalance()); // 1000
// console.log(account.balance);    // Error! Property 'balance' is private

Getters and Setters

Getters and setters let you control access to properties:

class Circle {
  private _radius: number;

  constructor(radius: number) {
    this._radius = radius;
  }

  // Getter - accessed like a property
  get radius(): number {
    return this._radius;
  }

  // Setter - validates before setting
  set radius(value: number) {
    if (value <= 0) {
      throw new Error('Radius must be positive');
    }
    this._radius = value;
  }

  // Computed property with getter
  get area(): number {
    return Math.PI * this._radius ** 2;
  }

  get circumference(): number {
    return 2 * Math.PI * this._radius;
  }
}

const circle = new Circle(5);
console.log(circle.radius); // 5 (uses getter)
console.log(circle.area); // 78.54... (computed)
console.log(circle.circumference); // 31.42...

circle.radius = 10; // Uses setter
console.log(circle.radius); // 10

// circle.radius = -5;            // Error! "Radius must be positive"

When to Use Getters and Setters

  1. Validation: Check values before setting
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;
console.log(temp.fahrenheit); // 77

temp.fahrenheit = 32;
console.log(temp.celsius); // 0
  1. Computed properties: Calculate values on demand
class FullName {
  constructor(
    public firstName: string,
    public lastName: string
  ) {}

  get fullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  set fullName(value: string) {
    const parts = value.split(' ');
    this.firstName = parts[0] || '';
    this.lastName = parts.slice(1).join(' ');
  }
}

const name = new FullName('Alice', 'Smith');
console.log(name.fullName); // "Alice Smith"

name.fullName = 'Bob Johnson Jr';
console.log(name.firstName); // "Bob"
console.log(name.lastName); // "Johnson Jr"

Static Members

Static members belong to the class itself, not to instances:

class MathUtils {
  static PI: number = 3.14159;

  static square(x: number): number {
    return x * x;
  }

  static cube(x: number): number {
    return x * x * x;
  }

  static circleArea(radius: number): number {
    return MathUtils.PI * radius ** 2;
  }
}

// Access static members on the class, not instances
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.square(5)); // 25
console.log(MathUtils.cube(3)); // 27
console.log(MathUtils.circleArea(5)); // 78.54...

// Cannot access static members on instances
const utils = new MathUtils();
// console.log(utils.PI);            // Error! Property 'PI' does not exist

Static vs Instance Members

class Counter {
  // Static - shared by all instances
  static totalCreated: number = 0;

  // Instance - unique to each instance
  value: number = 0;

  constructor() {
    Counter.totalCreated++;
  }

  increment(): void {
    this.value++;
  }

  static resetTotal(): void {
    Counter.totalCreated = 0;
  }
}

const counter1 = new Counter();
const counter2 = new Counter();
const counter3 = new Counter();

console.log(Counter.totalCreated); // 3

counter1.increment();
counter1.increment();
counter2.increment();

console.log(counter1.value); // 2
console.log(counter2.value); // 1
console.log(counter3.value); // 0

Practical Examples

Example 1: Bank Account

class BankAccount {
  private balance: number;
  private transactions: string[] = [];

  constructor(
    public readonly accountNumber: string,
    public ownerName: string,
    initialDeposit: number = 0
  ) {
    this.balance = initialDeposit;
    if (initialDeposit > 0) {
      this.transactions.push(`Initial deposit: $${initialDeposit}`);
    }
  }

  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error('Deposit amount must be positive');
    }
    this.balance += amount;
    this.transactions.push(`Deposit: $${amount}`);
  }

  withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error('Withdrawal amount must be positive');
    }
    if (amount > this.balance) {
      throw new Error('Insufficient funds');
    }
    this.balance -= amount;
    this.transactions.push(`Withdrawal: $${amount}`);
  }

  getBalance(): number {
    return this.balance;
  }

  getStatement(): string[] {
    return [...this.transactions];
  }
}

const account = new BankAccount('123456', 'Alice', 1000);
account.deposit(500);
account.withdraw(200);

console.log(account.getBalance()); // 1300
console.log(account.getStatement());
// ["Initial deposit: $1000", "Deposit: $500", "Withdrawal: $200"]

Example 2: Shopping Cart Item

class CartItem {
  constructor(
    public readonly id: number,
    public name: string,
    public price: number,
    private _quantity: number = 1
  ) {}

  get quantity(): number {
    return this._quantity;
  }

  set quantity(value: number) {
    if (value < 0) {
      throw new Error('Quantity cannot be negative');
    }
    this._quantity = value;
  }

  get total(): number {
    return this.price * this._quantity;
  }

  increment(): void {
    this._quantity++;
  }

  decrement(): void {
    if (this._quantity > 0) {
      this._quantity--;
    }
  }
}

const item = new CartItem(1, 'Laptop', 999);
console.log(item.total); // 999

item.quantity = 2;
console.log(item.total); // 1998

item.increment();
console.log(item.quantity); // 3
console.log(item.total); // 2997

Example 3: Logger with Levels

class Logger {
  static instance: Logger;

  private logs: string[] = [];

  constructor(public name: string) {}

  private formatMessage(level: string, message: string): string {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] [${level}] [${this.name}] ${message}`;
  }

  info(message: string): void {
    const formatted = this.formatMessage('INFO', message);
    this.logs.push(formatted);
    console.log(formatted);
  }

  warn(message: string): void {
    const formatted = this.formatMessage('WARN', message);
    this.logs.push(formatted);
    console.warn(formatted);
  }

  error(message: string): void {
    const formatted = this.formatMessage('ERROR', message);
    this.logs.push(formatted);
    console.error(formatted);
  }

  getLogs(): string[] {
    return [...this.logs];
  }

  clear(): void {
    this.logs = [];
  }
}

const logger = new Logger('App');
logger.info('Application started');
logger.warn('Memory usage high');
logger.error('Connection failed');

console.log(logger.getLogs().length); // 3

Exercises

Exercise 1: Rectangle Class

Create a Rectangle class with:

  • Properties: width and height
  • Methods: getArea(), getPerimeter(), isSquare()
  • A scale() method that multiplies both dimensions
Solution
class Rectangle {
  constructor(
    public width: number,
    public height: number
  ) {}

  getArea(): number {
    return this.width * this.height;
  }

  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }

  isSquare(): boolean {
    return this.width === this.height;
  }

  scale(factor: number): void {
    this.width *= factor;
    this.height *= factor;
  }
}

const rect = new Rectangle(10, 20);
console.log(rect.getArea()); // 200
console.log(rect.getPerimeter()); // 60
console.log(rect.isSquare()); // false

rect.scale(2);
console.log(rect.width); // 20
console.log(rect.height); // 40

const square = new Rectangle(5, 5);
console.log(square.isSquare()); // true

Exercise 2: Counter with Bounds

Create a BoundedCounter class with:

  • A value that starts at 0
  • Min and max bounds (set in constructor)
  • Methods: increment(), decrement(), reset()
  • The value should never go below min or above max
Solution
class BoundedCounter {
  private _value: number;

  constructor(
    private min: number = 0,
    private max: number = 100,
    initialValue: number = 0
  ) {
    this._value = Math.max(min, Math.min(max, initialValue));
  }

  get value(): number {
    return this._value;
  }

  increment(): void {
    if (this._value < this.max) {
      this._value++;
    }
  }

  decrement(): void {
    if (this._value > this.min) {
      this._value--;
    }
  }

  reset(): void {
    this._value = this.min;
  }
}

const counter = new BoundedCounter(0, 5);
console.log(counter.value); // 0

counter.increment();
counter.increment();
counter.increment();
console.log(counter.value); // 3

counter.increment();
counter.increment();
counter.increment(); // This won't go above 5
console.log(counter.value); // 5

counter.decrement();
console.log(counter.value); // 4

counter.reset();
console.log(counter.value); // 0

Exercise 3: Simple Library Book

Create a Book class with:

  • Properties: title, author, isbn (readonly), isAvailable
  • Method checkout() - marks as unavailable if available
  • Method return() - marks as available
  • Use a getter for a status property that returns "Available" or "Checked out"
Solution
class Book {
  private isAvailable: boolean = true;

  constructor(
    public title: string,
    public author: string,
    public readonly isbn: string
  ) {}

  get status(): string {
    return this.isAvailable ? 'Available' : 'Checked out';
  }

  checkout(): boolean {
    if (this.isAvailable) {
      this.isAvailable = false;
      return true;
    }
    return false;
  }

  return(): void {
    this.isAvailable = true;
  }
}

const book = new Book('TypeScript Handbook', 'Microsoft', '978-0-123456-78-9');

console.log(book.status); // "Available"
console.log(book.checkout()); // true
console.log(book.status); // "Checked out"
console.log(book.checkout()); // false (already checked out)

book.return();
console.log(book.status); // "Available"

Exercise 4: ID Generator

Create a IdGenerator class with:

  • A static counter that increments with each new ID
  • A static method generate() that returns the next ID as a string with a prefix
  • A static method reset() to reset the counter
Solution
class IdGenerator {
  private static counter: number = 0;
  private static prefix: string = 'ID';

  static generate(): string {
    IdGenerator.counter++;
    return `${IdGenerator.prefix}-${IdGenerator.counter.toString().padStart(4, '0')}`;
  }

  static reset(): void {
    IdGenerator.counter = 0;
  }

  static setPrefix(prefix: string): void {
    IdGenerator.prefix = prefix;
  }
}

console.log(IdGenerator.generate()); // "ID-0001"
console.log(IdGenerator.generate()); // "ID-0002"
console.log(IdGenerator.generate()); // "ID-0003"

IdGenerator.setPrefix('USER');
console.log(IdGenerator.generate()); // "USER-0004"

IdGenerator.reset();
console.log(IdGenerator.generate()); // "USER-0001"

Key Takeaways

  1. Classes bundle data and behavior: Properties hold data, methods define actions
  2. Constructor initializes objects: Called when using new ClassName()
  3. Parameter properties reduce boilerplate: constructor(public name: string)
  4. Getters and setters control access: Validate input, compute values
  5. Static members belong to the class: Shared across all instances
  6. Readonly prevents changes: Can only be set in constructor

Resources

Resource Type Description
TypeScript Handbook: Classes Documentation Official guide to classes
MDN: Classes Documentation JavaScript class fundamentals
TypeScript Playground Tool Try class examples online

Next Lesson

Now that you can create basic classes, let us learn about inheritance and access modifiers to build more sophisticated class hierarchies.

Continue to Lesson 4.5: Inheritance and Access Modifiers