I'm Samuel FajreldinesI am a specialist in the entire JavaScript and TypeScript ecosystem (including Node.js, React, Angular and Vue.js) I am expert in AI and in creating AI integrated solutions I am expert in DevOps and Serverless Architecture (AWS, Google Cloud and Azure) I am expert in PHP and its frameworks (such as Codeigniter and Laravel). |
Samuel FajreldinesI am a specialist in the entire JavaScript and TypeScript ecosystem. I am expert in AI and in creating AI integrated solutions. I am expert in DevOps and Serverless Architecture I am expert in PHP and its frameworks.
|
Introduction
In the ever-evolving world of web development, TypeScript has emerged as a powerful tool for building robust and scalable applications. Its strong typing system enhances JavaScript by adding static types, which help catch errors early in the development process. One of the most powerful features of TypeScript is the ability to use generics, particularly generic classes, which enable you to create reusable components that work with a variety of data types.
In backend development, generic classes can significantly improve the flexibility and maintainability of your codebase. This comprehensive guide will explore how to effectively use generic classes in TypeScript for backend development, helping you write cleaner, more efficient, and type-safe code.
Table of Contents
Generics allow you to create reusable components that work with a variety of types rather than a single one. This ensures type safety while enabling flexibility.
Basic Generic Function Example:
function identity<T>(arg: T): T {
return arg;
}
Here, T
is a type parameter that acts as a placeholder for the type that will be provided when the function is called.
Using generic classes in backend development offers several advantages:
A generic class allows you to define a class with a placeholder for the type of its properties or methods.
Generic Class Syntax:
class GenericClass<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
Usage Example:
const numberInstance = new GenericClass<number>(42);
console.log(numberInstance.getValue()); // Output: 42
const stringInstance = new GenericClass<string>("Hello, World!");
console.log(stringInstance.getValue()); // Output: Hello, World!
In backend applications, repositories are used to encapsulate data access logic. Using generics, you can create a single repository class that works with different models.
Defining a Generic Repository Interface:
interface IRepository<T> {
getById(id: string): Promise<T | null>;
getAll(): Promise<T[]>;
add(entity: T): Promise<void>;
update(entity: T): Promise<void>;
delete(id: string): Promise<void>;
}
Implementing a Generic Repository Class:
class Repository<T> implements IRepository<T> {
private collection: string;
constructor(collection: string) {
this.collection = collection;
}
async getById(id: string): Promise<T | null> {
// Implement data retrieval logic here
return null;
}
async getAll(): Promise<T[]> {
// Implement data retrieval logic here
return [];
}
async add(entity: T): Promise<void> {
// Implement data insertion logic here
}
async update(entity: T): Promise<void> {
// Implement data update logic here
}
async delete(id: string): Promise<void> {
// Implement data deletion logic here
}
}
Using the Generic Repository:
interface User {
id: string;
name: string;
email: string;
}
const userRepository = new Repository<User>("users");
userRepository.add({ id: "1", name: "Alice", email: "alice@example.com" });
Sometimes, you need to ensure that a type parameter meets certain conditions. Type constraints allow you to restrict generics to types that have certain properties or methods.
Using the extends
Keyword:
interface Identifiable {
id: string;
}
class BaseEntityRepository<T extends Identifiable> {
// Implementation...
}
Now, T
must be a type that has an id
property of type string
.
Example:
class Product implements Identifiable {
id: string;
price: number;
}
const productRepository = new BaseEntityRepository<Product>();
You can define multiple type parameters for more complex scenarios.
Example:
class MapEntry<K, V> {
key: K;
value: V;
constructor(key: K, value: V) {
this.key = key;
this.value = value;
}
}
const entry = new MapEntry<number, string>(1, "One");
You can assign default types to type parameters.
Example:
class Response<T = any> {
data: T;
status: number;
constructor(data: T, status: number) {
this.data = data;
this.status = status;
}
}
Create a generic error response class to standardize error handling.
class ErrorResponse<T> {
error: T;
message: string;
constructor(error: T, message: string) {
this.error = error;
this.message = message;
}
}
interface ValidationError {
field: string;
issue: string;
}
const validationErrorResponse = new ErrorResponse<ValidationError>(
{ field: "email", issue: "Invalid format" },
"Validation Error"
);
Implement reusable middleware functions using generics.
function genericMiddleware<T>(req: Request, res: Response, next: NextFunction) {
// Middleware logic using generic type T
next();
}
Use Meaningful Type Parameter Names: Instead of T
, use descriptive names like TEntity
, TModel
.
Keep It Simple: Don't overcomplicate generics; use them when they add clear value.
Combine with Other TypeScript Features: Leverage interfaces, type unions, and intersection types with generics for more robust code.
Comment Your Code: Provide explanations for complex generic implementations to aid understanding.
Type parameters are not available at runtime due to TypeScript's type erasure. Avoid relying on type information at runtime within generic classes.
Incorrect Approach:
class Factory<T> {
create(): T {
return new T(); // Error: 'T' cannot be used as a value
}
}
Solution:
Provide the class type as a parameter.
class Factory<T> {
type: new () => T;
constructor(type: new () => T) {
this.type = type;
}
create(): T {
return new this.type();
}
}
class User {
name: string;
}
const userFactory = new Factory(User);
const user = userFactory.create();
Don't use generics when not necessary, as they can make the code harder to read.
Example of Overuse:
function logValue<T>(value: T): void {
console.log(value);
}
// Not needed; simpler function suffices
function logValue(value: any): void {
console.log(value);
}
Generics in TypeScript are a potent tool for creating reusable and maintainable code, especially in backend development. By mastering generic classes, you can build flexible data structures, implement design patterns like repositories and services, and write code that elegantly handles various data types.
Remember to apply constraints thoughtfully, follow best practices, and be cautious of common pitfalls. As you harness the power of generics, your TypeScript backend applications will become more robust, scalable, and efficient.
References
About Me
Since I was a child, I've always wanted to be an inventor. As I grew up, I specialized in information systems, an area which I fell in love with and live around it. I am a full-stack developer and work a lot with devops, i.e., I'm a kind of "jack-of-all-trades" in IT. Wherever there is something cool or new, you'll find me exploring and learning... I am passionate about life, family, and sports. I believe that true happiness can only be achieved by balancing these pillars. I am always looking for new challenges and learning opportunities, and would love to connect with other technology professionals to explore possibilities for collaboration. If you are looking for a dedicated and committed full-stack developer with a passion for excellence, please feel free to contact me. It would be a pleasure to talk with you! |
SecurityScoreCard
Nov. 2023 - Present
New York, United States
Senior Software Engineer
I joined SecurityScorecard, a leading organization with over 400 employees, as a Senior Full Stack Software Engineer. My role spans across developing new systems, maintaining and refactoring legacy solutions, and ensuring they meet the company's high standards of performance, scalability, and reliability.
I work across the entire stack, contributing to both frontend and backend development while also collaborating directly on infrastructure-related tasks, leveraging cloud computing technologies to optimize and scale our systems. This broad scope of responsibilities allows me to ensure seamless integration between user-facing applications and underlying systems architecture.
Additionally, I collaborate closely with diverse teams across the organization, aligning technical implementation with strategic business objectives. Through my work, I aim to deliver innovative and robust solutions that enhance SecurityScorecard's offerings and support its mission to provide world-class cybersecurity insights.
Technologies Used:
Node.js Terraform React Typescript AWS Playwright and Cypress