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
- 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
- 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:
widthandheight - 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
statusproperty 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
- Classes bundle data and behavior: Properties hold data, methods define actions
- Constructor initializes objects: Called when using
new ClassName() - Parameter properties reduce boilerplate:
constructor(public name: string) - Getters and setters control access: Validate input, compute values
- Static members belong to the class: Shared across all instances
- 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.