Logo

Home

About

Blog

Contact

Buy Merch

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Classes in TypeScript: The Basics

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Function Overloading in TypeScript
TypeScript
9m
Dec 28, 2025

Function Overloading in TypeScript

Function overloading allows you to define multiple function signatures for a single implementation in TypeScript. This guide covers how to write overloaded functions, the implementation signature, and practical use cases to improve type safety and clarity.

#TypeScript#Functions+3
Basic Types in TypeScript
TypeScript
9m
Nov 23, 2025

Basic Types in TypeScript

TypeScript's type system starts with fundamental building blocks: primitive types like string, number, and boolean, along with arrays and type annotations. Understanding these basic types is essential for writing type-safe code. This comprehensive guide walks you through each primitive type, shows you how to use type annotations effectively, and teaches you best practices for working with arrays and basic data structures in TypeScript.

#TypeScript#Types+5
Introduction to TypeScript: Why Static Typing Matters
TypeScript
7m
Nov 6, 2025

Introduction to TypeScript: Why Static Typing Matters

TypeScript has evolved from a Microsoft experiment to the industry standard for building scalable JavaScript applications. Understanding why static typing matters is the first step in mastering modern web development. This comprehensive introduction explores the benefits, use cases, and real-world impact of adopting TypeScript.

#TypeScript#JavaScript+6
Ask me anything! 💬

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2026 • All rights reserved

Contents

  • Introduction to Classes
  • Why Classes Matter in TypeScript
  • Your First TypeScript Class
  • Understanding Constructors
  • Class Methods
  • Creating and Using Instances
  • Practical Example: Building a Todo Manager
  • Common Patterns and Best Practices
TypeScript

Classes in TypeScript: The Basics

February 8, 2026•14 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript class diagram showing properties, methods, and constructors
Classes in TypeScript: The Basics

Introduction to Classes

Classes are the foundation of object-oriented programming in TypeScript. If you've worked with JavaScript classes, TypeScript classes will feel familiar—but with the added power of static type checking that catches errors before runtime.

Think of a class as a blueprint for creating objects. Just like an architectural blueprint defines how to build a house, a class defines the structure and behavior of objects you create from it. TypeScript enhances this blueprint with type annotations, ensuring every object you create follows the exact specifications you define.

In this guide, we'll build your understanding from the ground up. You'll learn how to define properties, write methods, use constructors, and create instances—all with TypeScript's type safety keeping you on track.

Why Classes Matter in TypeScript

Before diving into syntax, let's understand why classes are particularly valuable in TypeScript:

Type Safety for Object State: Classes let you define exactly what properties an object should have and what types those properties should be. TypeScript enforces this at compile time.

Encapsulation: Group related data and behavior together. A User class keeps user data and user-related methods in one logical unit.

Code Reusability: Write a class once, create hundreds of instances. Each instance gets the same methods and property structure without duplicating code.

Familiar to OOP Developers: If you come from Java, C#, or other object-oriented languages, TypeScript classes provide a familiar paradigm with JavaScript's flexibility.

Better IDE Support: TypeScript's type system combined with classes gives you autocomplete, inline documentation, and refactoring tools that JavaScript alone can't match.

Coming from JavaScript?

If you've used ES6 classes in JavaScript, TypeScript classes will feel familiar—but with the added benefit of compile-time type checking that catches errors before your code runs.

Your First TypeScript Class

Let's start with the simplest possible class and build from there:

class User {
  name: string;
  email: string;
  age: number;
}

This defines a User class with three properties. Notice the type annotations—name and email are strings, age is a number. This is where TypeScript shines: the compiler will prevent you from assigning the wrong type to these properties.

But there's a problem with this code. Try to create an instance:

const user = new User();
// Error: Property 'name' has no initializer and is not definitely assigned

TypeScript is telling us these properties need initial values. We have two ways to fix this: provide default values or use a constructor.

Option 1: Default Values

class User {
  name: string = "";
  email: string = "";
  age: number = 0;
}
 
const user = new User();
console.log(user.name); // ""
console.log(user.age); // 0

This works, but every user starts with empty strings and zero. Not very useful for real applications.

Option 2: Constructor (Better Approach)

class User {
  name: string;
  email: string;
  age: number;
 
  constructor(name: string, email: string, age: number) {
    this.name = name;
    this.email = email;
    this.age = age;
  }
}
 
const user = new User("Alice Johnson", "[email protected]", 28);
console.log(user.name); // "Alice Johnson"
console.log(user.age); // 28

Much better! Now we can create users with specific data. The constructor is a special method that runs when you create a new instance with the new keyword.

class User

Properties
🌐name
string
🌐email
string
🌐age
number
Methods
constructor(name: string, email: string, age: number)→ void
🌐public🔒private🛡️protected

Understanding Constructors

The constructor is your class's initialization method. It's called automatically when you instantiate the class with new. Let's break down what's happening:

class User {
  name: string;
  email: string;
  age: number;
 
  constructor(name: string, email: string, age: number) {
    // 'this' refers to the new instance being created
    this.name = name; // Assign parameter to instance property
    this.email = email;
    this.age = age;
  }
}

Key points:

  1. The constructor method must be named constructor
  2. Parameters have their own type annotations
  3. this refers to the instance being created
  4. You assign parameters to instance properties using this.propertyName

Constructor Validation

Constructors are a perfect place to validate data before creating an object:

class User {
  name: string;
  email: string;
  age: number;
 
  constructor(name: string, email: string, age: number) {
    if (age < 0 || age > 150) {
      throw new Error("Age must be between 0 and 150");
    }
 
    if (!email.includes("@")) {
      throw new Error("Invalid email address");
    }
 
    this.name = name;
    this.email = email;
    this.age = age;
  }
}
 
// This works
const validUser = new User("Bob Smith", "[email protected]", 35);
 
// This throws an error
const invalidUser = new User("Eve", "not-an-email", 200);
// Error: Age must be between 0 and 150

Best Practice

Always validate data in constructors to prevent invalid objects from being created. It's easier to catch bad data at creation time than to debug issues later when an object is in an invalid state.

Optional Constructor Parameters

You can make constructor parameters optional or provide defaults:

class User {
  name: string;
  email: string;
  age: number;
  verified: boolean;
 
  constructor(
    name: string,
    email: string,
    age: number,
    verified: boolean = false // Default parameter
  ) {
    this.name = name;
    this.email = email;
    this.age = age;
    this.verified = verified;
  }
}
 
const user1 = new User("Alice", "[email protected]", 28);
console.log(user1.verified); // false (default value)
 
const user2 = new User("Bob", "[email protected]", 35, true);
console.log(user2.verified); // true (provided value)

Class Methods

Properties store data, methods define behavior. A method is a function that belongs to a class and operates on the class's data:

class User {
  name: string;
  email: string;
  age: number;
 
  constructor(name: string, email: string, age: number) {
    this.name = name;
    this.email = email;
    this.age = age;
  }
 
  // Method: returns a greeting string
  greet(): string {
    return `Hello, my name is ${this.name}`;
  }
 
  // Method: returns boolean
  isAdult(): boolean {
    return this.age >= 18;
  }
 
  // Method: void return (performs action, returns nothing)
  celebrateBirthday(): void {
    this.age += 1;
    console.log(`Happy birthday! Now ${this.age} years old.`);
  }
}
 
const user = new User("Alice", "[email protected]", 28);
 
console.log(user.greet()); // "Hello, my name is Alice"
console.log(user.isAdult()); // true
 
user.celebrateBirthday(); // "Happy birthday! Now 29 years old."
console.log(user.age); // 29

Methods with Parameters

Methods can accept parameters just like regular functions:

class ShoppingCart {
  items: string[] = [];
  total: number = 0;
 
  addItem(itemName: string, price: number): void {
    this.items.push(itemName);
    this.total += price;
  }
 
  removeItem(itemName: string, price: number): boolean {
    const index = this.items.indexOf(itemName);
 
    if (index === -1) {
      return false; // Item not found
    }
 
    this.items.splice(index, 1);
    this.total -= price;
    return true; // Successfully removed
  }
 
  getItemCount(): number {
    return this.items.length;
  }
 
  getSummary(): string {
    return `Cart contains ${this.items.length} items totaling \$${this.total.toFixed(2)}`;
  }
}
 
const cart = new ShoppingCart();
cart.addItem("Laptop", 1200);
cart.addItem("Mouse", 25);
cart.addItem("Keyboard", 75);
 
console.log(cart.getSummary());
// "Cart contains 3 items totaling \$1300.00"
 
cart.removeItem("Mouse", 25);
console.log(cart.getSummary());
// "Cart contains 2 items totaling \$1275.00"

Method Return Types

TypeScript infers return types, but it's best practice to explicitly declare them:

class Calculator {
  // Explicit return type: number
  add(a: number, b: number): number {
    return a + b;
  }
 
  // Explicit return type: string
  formatResult(value: number): string {
    return `Result: ${value}`;
  }
 
  // Explicit return type: void (returns nothing)
  logResult(value: number): void {
    console.log(`The result is: ${value}`);
  }
 
  // Explicit return type: boolean
  isPositive(value: number): boolean {
    return value > 0;
  }
}

Creating and Using Instances

An instance is an individual object created from a class. Each instance has its own property values but shares the same methods:

class BankAccount {
  accountNumber: string;
  balance: number;
  owner: string;
 
  constructor(
    accountNumber: string,
    owner: string,
    initialBalance: number = 0
  ) {
    this.accountNumber = accountNumber;
    this.owner = owner;
    this.balance = initialBalance;
  }
 
  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("Deposit amount must be positive");
    }
    this.balance += amount;
  }
 
  withdraw(amount: number): boolean {
    if (amount <= 0) {
      throw new Error("Withdrawal amount must be positive");
    }
 
    if (amount > this.balance) {
      return false; // Insufficient funds
    }
 
    this.balance -= amount;
    return true;
  }
 
  getBalance(): number {
    return this.balance;
  }
}
 
// Create multiple instances
const account1 = new BankAccount("ACC001", "Alice Johnson", 1000);
const account2 = new BankAccount("ACC002", "Bob Smith", 500);
const account3 = new BankAccount("ACC003", "Charlie Davis");
 
// Each instance has independent state
account1.deposit(500);
account2.withdraw(200);
 
console.log(account1.getBalance()); // 1500
console.log(account2.getBalance()); // 300
console.log(account3.getBalance()); // 0
 
// They're completely separate objects
console.log(account1.owner); // "Alice Johnson"
console.log(account2.owner); // "Bob Smith"

Instance vs Class

This is crucial to understand:

  • Class: The blueprint (definition of structure and behavior)
  • Instance: An actual object created from that blueprint
class Car {
  brand: string;
  model: string;
  year: number;
 
  constructor(brand: string, model: string, year: number) {
    this.brand = brand;
    this.model = model;
    this.year = year;
  }
 
  getAge(): number {
    const currentYear = new Date().getFullYear();
    return currentYear - this.year;
  }
}
 
// Car is the class (blueprint)
// car1, car2, car3 are instances (actual objects)
 
const car1 = new Car("Toyota", "Camry", 2020);
const car2 = new Car("Honda", "Civic", 2018);
const car3 = new Car("Ford", "Mustang", 2022);
 
console.log(car1.getAge()); // Approximately 6 (in 2026)
console.log(car2.getAge()); // Approximately 8
console.log(car3.getAge()); // Approximately 4

Each instance has the same methods (getAge()) but different property values (brand, model, year).

Type Annotations for Class Instances

When you create a variable to hold a class instance, TypeScript infers its type:

class Product {
  name: string;
  price: number;
 
  constructor(name: string, price: number) {
    this.name = name;
    this.price = price;
  }
 
  applyDiscount(percentage: number): void {
    this.price = this.price * (1 - percentage / 100);
  }
}
 
// TypeScript infers the type as Product
const laptop = new Product("Laptop", 1200);
 
// You can also explicitly annotate
const phone: Product = new Product("Phone", 800);
 
// This helps catch errors
const tablet: Product = {
  name: "Tablet",
  price: 400,
  // Error: Missing method applyDiscount
};

Using Classes as Types

Classes can be used as types in function parameters and return types:

class Invoice {
  invoiceNumber: string;
  amount: number;
  isPaid: boolean = false;
 
  constructor(invoiceNumber: string, amount: number) {
    this.invoiceNumber = invoiceNumber;
    this.amount = amount;
  }
 
  markAsPaid(): void {
    this.isPaid = true;
  }
 
  getStatus(): string {
    return this.isPaid ? "Paid" : "Unpaid";
  }
}
 
// Function accepting Invoice instance as parameter
function processInvoice(invoice: Invoice): void {
  if (!invoice.isPaid) {
    console.log(`Processing payment for invoice ${invoice.invoiceNumber}`);
    invoice.markAsPaid();
  }
}
 
// Function returning Invoice instance
function createInvoice(number: string, amount: number): Invoice {
  return new Invoice(number, amount);
}
 
// Usage
const inv1 = createInvoice("INV-001", 1500);
processInvoice(inv1);
console.log(inv1.getStatus()); // "Paid"

Working with Arrays of Instances

Real applications often deal with collections of objects. TypeScript makes this type-safe:

class Task {
  title: string;
  completed: boolean = false;
  priority: number;
 
  constructor(title: string, priority: number) {
    this.title = title;
    this.priority = priority;
  }
 
  complete(): void {
    this.completed = true;
  }
 
  getDescription(): string {
    const status = this.completed ? "✓" : "○";
    return `${status} [P${this.priority}] ${this.title}`;
  }
}
 
// Array of Task instances
const tasks: Task[] = [
  new Task("Write blog post", 1),
  new Task("Review pull requests", 2),
  new Task("Update documentation", 3),
];
 
// Type-safe array operations
tasks.forEach((task) => {
  console.log(task.getDescription());
  // TypeScript knows 'task' is a Task instance
  // You get autocomplete for all Task methods
});
 
// Mark high-priority tasks as complete
tasks.filter((task) => task.priority === 1).forEach((task) => task.complete());
 
// Get all incomplete tasks
const incompleteTasks = tasks.filter((task) => !task.completed);
console.log(`${incompleteTasks.length} tasks remaining`);

Practical Example: Building a Todo Manager

Let's combine everything we've learned into a practical example:

class TodoItem {
  id: number;
  title: string;
  description: string;
  completed: boolean = false;
  createdAt: Date;
 
  constructor(id: number, title: string, description: string) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.createdAt = new Date();
  }
 
  toggle(): void {
    this.completed = !this.completed;
  }
 
  update(title: string, description: string): void {
    this.title = title;
    this.description = description;
  }
 
  getAge(): number {
    const now = new Date().getTime();
    const created = this.createdAt.getTime();
    const ageInDays = Math.floor((now - created) / (1000 * 60 * 60 * 24));
    return ageInDays;
  }
}
 
class TodoList {
  items: TodoItem[] = [];
  private nextId: number = 1;
 
  addItem(title: string, description: string): TodoItem {
    const item = new TodoItem(this.nextId++, title, description);
    this.items.push(item);
    return item;
  }
 
  removeItem(id: number): boolean {
    const index = this.items.findIndex((item) => item.id === id);
 
    if (index === -1) {
      return false;
    }
 
    this.items.splice(index, 1);
    return true;
  }
 
  toggleItem(id: number): boolean {
    const item = this.items.find((item) => item.id === id);
 
    if (!item) {
      return false;
    }
 
    item.toggle();
    return true;
  }
 
  getCompleted(): TodoItem[] {
    return this.items.filter((item) => item.completed);
  }
 
  getPending(): TodoItem[] {
    return this.items.filter((item) => !item.completed);
  }
 
  getStats(): { total: number; completed: number; pending: number } {
    return {
      total: this.items.length,
      completed: this.getCompleted().length,
      pending: this.getPending().length,
    };
  }
}
 
// Usage
const myTodos = new TodoList();
 
myTodos.addItem("Learn TypeScript classes", "Complete the basics tutorial");
myTodos.addItem("Build a project", "Apply class concepts to real code");
myTodos.addItem(
  "Review OOP principles",
  "Refresh on encapsulation and inheritance"
);
 
// Mark first item as complete
myTodos.toggleItem(1);
 
// Check stats
const stats = myTodos.getStats();
console.log(
  `Total: ${stats.total}, Completed: ${stats.completed}, Pending: ${stats.pending}`
);
// "Total: 3, Completed: 1, Pending: 2"
 
// Get pending items
const pending = myTodos.getPending();
pending.forEach((item) => {
  console.log(`TODO: ${item.title} (${item.getAge()} days old)`);
});

This example shows how classes enable you to:

  • Encapsulate related data (todo properties) with behavior (methods)
  • Manage collections of instances (TodoList managing TodoItems)
  • Create clean, maintainable code with clear responsibilities
  • Leverage TypeScript's type safety throughout

Common Patterns and Best Practices

1. Single Responsibility

Each class should have one clear purpose:

// Good: Each class has a single, clear responsibility
class User {
  name: string;
  email: string;
 
  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
}
 
class UserValidator {
  static isValidEmail(email: string): boolean {
    return email.includes("@") && email.includes(".");
  }
 
  static isValidName(name: string): boolean {
    return name.length >= 2 && name.length <= 100;
  }
}
 
// Bad: User class trying to do too much
class UserWithEverything {
  name: string;
  email: string;
 
  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
 
  validateEmail(): boolean {
    /* ... */
  }
  sendEmail(): void {
    /* ... */
  }
  saveToDatabase(): void {
    /* ... */
  }
  logActivity(): void {
    /* ... */
  }
  // Too many responsibilities!
}

2. Constructor Parameter Validation

Validate inputs early to catch errors:

class Temperature {
  celsius: number;
 
  constructor(celsius: number) {
    if (celsius < -273.15) {
      throw new Error("Temperature cannot be below absolute zero");
    }
    this.celsius = celsius;
  }
 
  toFahrenheit(): number {
    return (this.celsius * 9) / 5 + 32;
  }
 
  toKelvin(): number {
    return this.celsius + 273.15;
  }
}
 
// This works
const boiling = new Temperature(100);
 
// This fails fast with a clear error
const impossible = new Temperature(-300);
// Error: Temperature cannot be below absolute zero

3. Method Chaining

Return this to enable fluent interfaces:

class StringBuilder {
  private value: string = "";
 
  append(text: string): this {
    this.value += text;
    return this;
  }
 
  appendLine(text: string): this {
    this.value += text + "\n";
    return this;
  }
 
  clear(): this {
    this.value = "";
    return this;
  }
 
  toString(): string {
    return this.value;
  }
}
 
// Method chaining in action
const message = new StringBuilder()
  .appendLine("Dear Customer,")
  .appendLine("")
  .append("Thank you for your order. ")
  .append("Your items will ship soon.")
  .toString();
 
console.log(message);
// "Dear Customer,
//
// Thank you for your order. Your items will ship soon."

Method Chaining Pro Tip

Returning this from methods enables fluent interfaces that make your code more readable and expressive. This pattern is used in popular libraries like jQuery and D3.js.

4. Computed Properties

Use methods for values that depend on other properties:

class Rectangle {
  width: number;
  height: number;
 
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
 
  // Computed property (method)
  getArea(): number {
    return this.width * this.height;
  }
 
  // Computed property
  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }
 
  // Computed property
  isSquare(): boolean {
    return this.width === this.height;
  }
}
 
const rect = new Rectangle(10, 5);
console.log(rect.getArea()); // 50
console.log(rect.getPerimeter()); // 30
console.log(rect.isSquare()); // false
 
const square = new Rectangle(8, 8);
console.log(square.isSquare()); // true

What's Next

You now understand the fundamentals of TypeScript classes: properties, methods, constructors, and instances. But we've only scratched the surface. In the next posts, you'll learn:

  • Access modifiers (public, private, protected) for controlling property visibility
  • Getters and setters for computed and validated properties
  • Static members for class-level properties and methods
  • Inheritance for building class hierarchies
  • Abstract classes for creating blueprints
  • Interfaces and how classes implement them

Key Takeaways

Classes in TypeScript give you:

✅ Type-safe object blueprints with properties and methods ✅ Constructors for object initialization and validation ✅ Methods for encapsulating behavior ✅ Instances that share structure but maintain independent state ✅ Compile-time safety preventing common errors ✅ Better tooling with autocomplete and refactoring support

Start using classes for objects that have both data and behavior. As you build more complex applications, the structure and safety that classes provide become invaluable. Practice creating classes for real-world entities in your domain—users, products, orders, tasks—and you'll quickly internalize these concepts.