Skip to main content

TypeScript Classes

Introduction

Classes in TypeScript provide a powerful way to implement object-oriented programming (OOP) concepts in your Next.js applications. If you're coming from a language like Java, C#, or Python, you'll find TypeScript's class syntax familiar and comfortable. Even if you're only familiar with JavaScript, TypeScript classes extend JavaScript's ES6 class syntax with additional type safety and features.

In this tutorial, we'll learn how to define and use classes in TypeScript, including inheritance, access modifiers, abstract classes, and more.

Class Basics

Defining a Simple Class

Let's start by creating a basic class:

typescript
class Person {
name: string;
age: number;

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

greet() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}

// Creating an instance of the Person class
const alice = new Person("Alice", 28);
console.log(alice.greet());
// Output: Hello, my name is Alice and I am 28 years old.

In the example above:

  1. We define a Person class with two properties: name of type string and age of type number.
  2. We implement a constructor that initializes these properties.
  3. We add a greet method that returns a string introduction.
  4. We create an instance of the class and call its method.

Access Modifiers

TypeScript provides three access modifiers to control the visibility of class members:

  • public - Members are accessible from anywhere (default if not specified)
  • private - Members are only accessible within the class
  • protected - Members are accessible within the class and its subclasses

Let's modify our Person class to use access modifiers:

typescript
class Person {
private id: number;
protected birthYear: number;
public name: string;

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

public getAge(): number {
return new Date().getFullYear() - this.birthYear;
}

private generateId(): string {
return `PERSON-${this.id}`;
}

public getIdentifier(): string {
return this.generateId();
}
}

const bob = new Person(1, "Bob", 1990);
console.log(bob.name); // Accessible: Bob
console.log(bob.getAge()); // Accessible: 33 (in 2023)
// console.log(bob.id); // Error: Property 'id' is private
// console.log(bob.birthYear); // Error: Property 'birthYear' is protected
console.log(bob.getIdentifier()); // Accessible: PERSON-1

Parameter Properties

TypeScript provides a concise way to define and initialize class members in the constructor, known as parameter properties:

typescript
class Person {
constructor(
public name: string,
private age: number,
protected id: string
) {
// No need to assign these values manually
// TypeScript automatically creates and initializes the properties
}

public introduceYourself(): string {
return `Hi, I'm ${this.name}, age ${this.age}.`;
}
}

const charlie = new Person("Charlie", 35, "C123");
console.log(charlie.introduceYourself());
// Output: Hi, I'm Charlie, age 35.

This shorthand notation saves you from having to declare properties separately and then assign them in the constructor.

Readonly Properties

You can create read-only properties that can only be set during initialization:

typescript
class Circle {
readonly radius: number;

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

getArea(): number {
return Math.PI * this.radius * this.radius;
}
}

const myCircle = new Circle(5);
console.log(myCircle.radius); // 5
// myCircle.radius = 10; // Error: Cannot assign to 'radius' because it is a read-only property

Inheritance

TypeScript supports class inheritance, allowing you to create a hierarchy of classes:

typescript
class Person {
constructor(public name: string, protected age: number) {}

describe(): string {
return `Person named ${this.name}, age ${this.age}`;
}
}

class Employee extends Person {
constructor(name: string, age: number, private department: string) {
// Call the parent constructor using 'super'
super(name, age);
}

// Override the describe method
describe(): string {
return `${super.describe()}, works in ${this.department}`;
}

getDetails(): string {
return `${this.name} (${this.age}) - ${this.department}`;
}
}

const employee = new Employee("Diana", 32, "Engineering");
console.log(employee.describe());
// Output: Person named Diana, age 32, works in Engineering
console.log(employee.getDetails());
// Output: Diana (32) - Engineering

In this example:

  1. Employee extends Person, inheriting its properties and methods.
  2. The super keyword calls the parent constructor and parent methods.
  3. Employee overrides the describe method from Person.

Getters and Setters

TypeScript supports getters and setters to control access to class properties:

typescript
class BankAccount {
private _balance: number = 0;

get balance(): number {
// We could add validation or logging here
return this._balance;
}

set balance(value: number) {
if (value < 0) {
throw new Error("Balance cannot be negative");
}
this._balance = value;
}

deposit(amount: number): void {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
this.balance += 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;
}
}

const account = new BankAccount();
account.deposit(1000);
console.log(account.balance); // 1000
account.withdraw(500);
console.log(account.balance); // 500
// account.balance = -100; // Error: Balance cannot be negative

Static Members

Static properties and methods belong to the class itself, not instances of the class:

typescript
class MathUtils {
// Static property
static readonly PI: number = 3.14159;

// Static method
static calculateCircleArea(radius: number): number {
return MathUtils.PI * radius * radius;
}

// Instance method that uses static members
calculateCircumference(radius: number): number {
return 2 * MathUtils.PI * radius;
}
}

// Access static members without creating an instance
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircleArea(5)); // 78.53975

// Instance methods require an instance
const utils = new MathUtils();
console.log(utils.calculateCircumference(5)); // 31.4159

Abstract Classes

Abstract classes are base classes that cannot be instantiated directly but can be extended:

typescript
abstract class Shape {
constructor(protected color: string) {}

abstract calculateArea(): number;

abstract calculatePerimeter(): number;

describe(): string {
return `A ${this.color} shape with area ${this.calculateArea()}`;
}
}

class Rectangle extends Shape {
constructor(color: string, private width: number, private height: number) {
super(color);
}

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

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

// const shape = new Shape("red"); // Error: Cannot create an instance of an abstract class
const rectangle = new Rectangle("blue", 10, 5);
console.log(rectangle.describe()); // A blue shape with area 50
console.log(rectangle.calculatePerimeter()); // 30

Implementing Interfaces

Classes can implement interfaces to ensure they have specific properties and methods:

typescript
interface Drawable {
draw(): void;
getPosition(): { x: number; y: number };
}

class UIElement implements Drawable {
constructor(private x: number, private y: number, private element: string) {}

draw(): void {
console.log(`Drawing ${this.element} at position (${this.x}, ${this.y})`);
}

getPosition(): { x: number; y: number } {
return { x: this.x, y: this.y };
}
}

const button = new UIElement(100, 200, "Button");
button.draw();
// Output: Drawing Button at position (100, 200)

Real-World Example: Creating a Component System for Next.js

Let's create a simple component system that could be used in a Next.js application:

typescript
// Base Component class
abstract class Component {
protected id: string;

constructor(protected name: string) {
this.id = `component-${Math.random().toString(36).substr(2, 9)}`;
}

abstract render(): string;

getId(): string {
return this.id;
}

getName(): string {
return this.name;
}
}

// Button component
class Button extends Component {
constructor(
name: string,
private text: string,
private onClick: () => void
) {
super(name);
}

render(): string {
return `<button id="${this.id}" class="btn">${this.text}</button>`;
}

click(): void {
console.log(`Button ${this.name} clicked`);
this.onClick();
}
}

// Input field component
class InputField extends Component {
private value: string = "";

constructor(
name: string,
private placeholder: string,
private type: string = "text"
) {
super(name);
}

render(): string {
return `<input id="${this.id}" type="${this.type}" placeholder="${this.placeholder}" value="${this.value}" />`;
}

setValue(value: string): void {
this.value = value;
console.log(`Input ${this.name} value updated to: ${value}`);
}

getValue(): string {
return this.value;
}
}

// Form component that contains other components
class Form extends Component {
private components: Component[] = [];

addComponent(component: Component): void {
this.components.push(component);
}

render(): string {
const componentsHtml = this.components.map(c => c.render()).join('\n');
return `<form id="${this.id}" class="form">
${componentsHtml}
</form>`;
}
}

// Usage example
const loginForm = new Form("loginForm");

const usernameInput = new InputField("username", "Enter username");
const passwordInput = new InputField("password", "Enter password", "password");
const loginButton = new Button("loginButton", "Sign In", () => {
console.log(`Logging in as: ${usernameInput.getValue()}`);
});

loginForm.addComponent(usernameInput);
loginForm.addComponent(passwordInput);
loginForm.addComponent(loginButton);

// Simulate user input
usernameInput.setValue("john.doe");
passwordInput.setValue("secret123");

// Trigger button click
loginButton.click();

// Render the entire form
console.log(loginForm.render());

/* Output:
Input username value updated to: john.doe
Input password value updated to: secret123
Button loginButton clicked
Logging in as: john.doe
<form id="component-x7z3f9d1j" class="form">
<input id="component-a8b2c5e3d" type="text" placeholder="Enter username" value="john.doe" />
<input id="component-p4q7r2s9t" type="password" placeholder="Enter password" value="secret123" />
<button id="component-g1h6i3j8k" class="btn">Sign In</button>
</form>
*/

This example demonstrates several key concepts:

  • Abstract base classes
  • Inheritance
  • Access modifiers
  • Method overriding
  • Composition (Form containing other Components)

You could expand this system further for more complex UI components in your Next.js application.

Summary

TypeScript classes provide a powerful way to write object-oriented code with the benefits of static typing. In this tutorial, we covered:

  • Basic class syntax and creating instances
  • Access modifiers (public, private, protected)
  • Parameter properties for concise class definitions
  • Readonly properties
  • Inheritance with the extends keyword
  • Getters and setters
  • Static properties and methods
  • Abstract classes
  • Implementing interfaces
  • A real-world example of a component system

Classes in TypeScript are particularly useful when building robust Next.js applications, as they help organize code, ensure type safety, and enable reusability through inheritance and composition.

Exercises

  1. Create a User class with properties for username, email, and password, with appropriate access modifiers.
  2. Extend the User class to create an Admin class with additional permissions.
  3. Implement a simple state management class for a Next.js component.
  4. Create a utility class with static methods for common validation functions.
  5. Design a class hierarchy for different UI components in a Next.js application.

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)