This guide provides an in-depth exploration of key TypeScript concepts essential for backend developers, complete with examples, detailed explanations, and practical use cases. Use this as a reference for yourself and to train your team.
TypeScript allows developers to define the types of variables, function parameters, and return values. This ensures type safety and helps catch errors early during development.
let message: string = "Hello, TypeScript!";
function add(a: number, b: number): number {
return a + b;
}
const result = add(5, 10); // 15message: stringensures thatmessagecan only contain string values. Attempting to assign another type will trigger a compile-time error.add(a: number, b: number): numberenforces that the arguments and return value are numbers, ensuring consistency and preventing unexpected behavior.
Real-world Use Case: Enforcing strict types in API request handlers ensures that incoming and outgoing data adhere to expected formats, reducing bugs in production systems.
Interfaces define the structure of an object. They ensure consistency by requiring objects to conform to a specific shape.
interface User {
id: number;
name: string;
email: string;
}
function getUserInfo(user: User): string {
return `User: ${user.name}, Email: ${user.email}`;
}
const user: User = { id: 1, name: "John", email: "[email protected]" };
console.log(getUserInfo(user));- The
Userinterface specifies that any object of this type must haveid,name, andemailproperties, each with specific types. - Passing an object that does not conform to the interface structure will result in a TypeScript error.
Real-world Use Case: Interfaces are widely used to define the structure of database models or to validate API request payloads.
Generics provide a way to create reusable and flexible components while maintaining type safety.
function getFirstElement<T>(items: T[]): T {
return items[0];
}
const numbers = [1, 2, 3];
const strings = ["a", "b", "c"];
console.log(getFirstElement(numbers)); // 1
console.log(getFirstElement(strings)); // "a"<T>represents a placeholder type that is specified when the function is called.- This allows
getFirstElementto work with arrays of any type while preserving the type information.
Real-world Use Case: Use generics in repository patterns to handle CRUD operations for various database entities with type safety.
Classes encapsulate data and behavior into a single structure. Constructors are used to initialize object properties when creating instances of a class.
class Product {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
getDetails(): string {
return `${this.name} costs $${this.price}`;
}
}
const product = new Product("Laptop", 1200);
console.log(product.getDetails());- The
constructorinitializes thenameandpriceproperties when creating an instance ofProduct. - Methods like
getDetailsdefine the behavior associated with the class.
Real-world Use Case: Define service classes for handling business logic or models that represent database entities.
Access modifiers (public, private, protected) control the visibility and accessibility of class members.
class User {
private password: string;
constructor(password: string) {
this.password = password;
}
public getPassword(): string {
return this.password;
}
}
const user = new User("securePassword123");
console.log(user.getPassword()); // "securePassword123"privaterestricts access to within the class only.publicmakes methods or properties accessible from anywhere.
Real-world Use Case: Use access modifiers to encapsulate sensitive data like passwords or tokens and expose them only through secure methods.
Decorators are special functions that can modify classes, methods, or properties. They are extensively used in frameworks like NestJS for dependency injection and middleware.
function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Method ${propertyKey} called with arguments:`, args);
return originalMethod.apply(this, args);
};
}
class Calculator {
@LogMethod
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(5, 10); // Logs: "Method add called with arguments: [5, 10]"@LogMethodis a method decorator that intercepts method calls to add logging functionality.- Decorators can be applied to classes, properties, methods, and parameters to implement cross-cutting concerns like logging, validation, or authentication.
Real-world Use Case: Use decorators in controllers to handle validation or middleware in frameworks like NestJS.
Dependency Injection is a design pattern where dependencies are provided externally rather than being instantiated within a class. This enhances modularity and testability.
class DatabaseService {
connect() {
console.log("Connected to database.");
}
}
class UserService {
constructor(private dbService: DatabaseService) {}
getUserData() {
this.dbService.connect();
console.log("Fetching user data...");
}
}
const dbService = new DatabaseService();
const userService = new UserService(dbService);
userService.getUserData();- Instead of creating a
DatabaseServiceinstance insideUserService, it is injected through the constructor. - This separation of concerns makes it easier to test
UserServiceby injecting mocked dependencies.
Real-world Use Case: Manage services like database connections or logging in scalable backend systems using dependency injection frameworks like InversifyJS or the built-in DI system in NestJS.