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:
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:
- We define a
Person
class with two properties:name
of typestring
andage
of typenumber
. - We implement a constructor that initializes these properties.
- We add a
greet
method that returns a string introduction. - 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 classprotected
- Members are accessible within the class and its subclasses
Let's modify our Person
class to use access modifiers:
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:
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:
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:
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:
Employee
extendsPerson
, inheriting its properties and methods.- The
super
keyword calls the parent constructor and parent methods. Employee
overrides thedescribe
method fromPerson
.
Getters and Setters
TypeScript supports getters and setters to control access to class properties:
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:
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:
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:
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:
// 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
- Create a
User
class with properties for username, email, and password, with appropriate access modifiers. - Extend the
User
class to create anAdmin
class with additional permissions. - Implement a simple state management class for a Next.js component.
- Create a utility class with static methods for common validation functions.
- 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! :)