0
0
Typescriptprogramming~15 mins

Generic repository pattern in Typescript - Deep Dive

Choose your learning style9 modes available
Overview - Generic repository pattern
What is it?
The generic repository pattern is a way to organize code that handles data storage and retrieval in a reusable and consistent way. It uses a single, flexible class or interface to manage different types of data entities without rewriting the same code for each type. This pattern helps keep data access logic separate from business logic, making the code easier to maintain and test.
Why it matters
Without this pattern, developers often write repetitive code for each data type, which leads to mistakes and harder maintenance. The generic repository pattern solves this by providing a common way to handle data operations, saving time and reducing bugs. It also makes switching data sources or changing storage details easier, improving the overall quality and flexibility of software.
Where it fits
Before learning this, you should understand basic TypeScript, classes, interfaces, and how to work with data collections like arrays. After mastering this pattern, you can explore advanced data access techniques like unit of work, dependency injection, and ORMs (Object-Relational Mappers).
Mental Model
Core Idea
A generic repository acts like a universal manager that handles data operations for any type of object using one flexible set of rules.
Think of it like...
Imagine a universal remote control that can operate any TV brand by using the same buttons and commands, instead of having a different remote for each TV. The generic repository is like that remote for data operations.
┌─────────────────────────────┐
│       Generic Repository     │
│ ┌───────────────┐           │
│ │ Data Entity T │           │
│ └───────────────┘           │
│ ┌───────────────┐           │
│ │ CRUD Methods  │           │
│ │ (Create, Read,│           │
│ │ Update, Delete)│          │
│ └───────────────┘           │
└─────────────┬───────────────┘
              │
   ┌──────────┴───────────┐
   │                      │
┌───────┐             ┌────────┐
│ User  │             │ Product│
└───────┘             └────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding basic CRUD operations
🤔
Concept: Learn what CRUD means and how to perform these basic data operations.
CRUD stands for Create, Read, Update, and Delete. These are the four basic actions you can do with data. For example, adding a new user is Create, looking up a user is Read, changing user info is Update, and removing a user is Delete. In TypeScript, these can be simple functions that work on arrays or objects.
Result
You can write simple functions to add, find, change, or remove items from a list.
Knowing CRUD is essential because the repository pattern is built around these basic operations for any data type.
2
FoundationIntroduction to TypeScript generics
🤔
Concept: Learn how generics let you write flexible code that works with any data type.
Generics in TypeScript allow you to create functions or classes that can work with any type, without losing type safety. For example, a generic function can accept an array of any type and return an item from it. This avoids writing the same code for different types.
Result
You can write reusable functions and classes that adapt to different data types safely.
Understanding generics is key to building a repository that can handle many data types with one implementation.
3
IntermediateCreating a generic repository interface
🤔Before reading on: do you think a generic repository interface should include methods for all CRUD operations or only some? Commit to your answer.
Concept: Define a contract that any repository must follow, using generics to support any entity type.
We create an interface called IRepository with methods like add(item: T), get(id: string), update(item: T), and delete(id: string). This interface uses the generic type T to represent any data entity. This contract ensures all repositories behave consistently.
Result
A reusable interface that defines how to manage any data entity with standard methods.
Using an interface with generics enforces consistent behavior and allows different implementations to swap easily.
4
IntermediateImplementing a generic repository class
🤔Before reading on: do you think the generic repository class should store data in memory or connect directly to a database? Commit to your answer.
Concept: Build a class that implements the generic interface and manages data storage, here using an in-memory array for simplicity.
We create a class GenericRepository that implements IRepository. It uses an array to store items and provides methods to add, get, update, and delete items by id. This class works for any type T that has an id property.
Result
A working generic repository that can manage any data type in memory.
Implementing the repository shows how generics and interfaces combine to create flexible, reusable data managers.
5
IntermediateUsing constraints to ensure entity shape
🤔Before reading on: do you think the generic type T should have any restrictions? Commit to your answer.
Concept: Add constraints to generics so that the repository only accepts entities with certain properties, like an id.
We use TypeScript's extends keyword to require that T has an id property (e.g., interface IEntity { id: string }). This ensures the repository can identify and manage entities correctly.
Result
The repository only accepts entities that have an id, preventing errors at compile time.
Constraints improve safety and clarity by enforcing the minimum requirements for entities.
6
AdvancedExtending repository for specialized behavior
🤔Before reading on: do you think all repositories should have the same methods, or can some have extra features? Commit to your answer.
Concept: Show how to extend the generic repository to add custom methods for specific entity types.
For example, create a UserRepository that extends GenericRepository and adds a method findByEmail(email: string). This keeps common CRUD in the base class but allows special queries in subclasses.
Result
You get reusable base functionality plus custom features for specific data types.
Extending generic repositories balances reuse with flexibility for real-world needs.
7
ExpertIntegrating generic repository with dependency injection
🤔Before reading on: do you think dependency injection helps or complicates using generic repositories? Commit to your answer.
Concept: Use dependency injection to provide repository instances, improving testability and decoupling.
In a real app, you don't create repositories directly. Instead, you inject them where needed. This allows swapping implementations (e.g., in-memory vs database) without changing business logic. TypeScript decorators or frameworks like Inversify can help manage this.
Result
Your code becomes more modular, easier to test, and adaptable to changes in data storage.
Combining generic repositories with dependency injection is a powerful pattern for scalable, maintainable applications.
Under the Hood
The generic repository pattern works by using TypeScript generics to create a single class or interface that can operate on any data type. Internally, it stores data in a collection (like an array) and uses the generic type to enforce type safety. Methods like add, get, update, and delete manipulate this collection. Constraints ensure the generic type has necessary properties (like an id) so the repository can identify items. When used with dependency injection, the repository instance is provided to other parts of the app, allowing loose coupling.
Why designed this way?
This pattern was designed to reduce code duplication and enforce consistent data access methods across different entities. Before generics, developers wrote separate repositories for each type, leading to repetitive code and bugs. Generics allow one implementation to serve many types safely. The design also supports swapping data sources without changing business logic, improving maintainability and testing. Alternatives like writing separate classes or using dynamic typing were less safe or more error-prone.
┌───────────────────────────────┐
│       GenericRepository<T>     │
│ ┌───────────────────────────┐ │
│ │  data: T[]                │ │
│ │  add(item: T): void       │ │
│ │  get(id: string): T | null│ │
│ │  update(item: T): void    │ │
│ │  delete(id: string): void │ │
│ └─────────────┬─────────────┘ │
└───────────────│───────────────┘
                │
      ┌─────────┴─────────┐
      │                   │
┌─────────────┐     ┌─────────────┐
│ UserRepo    │     │ ProductRepo │
│ extends     │     │ extends     │
│ GenericRepo │     │ GenericRepo │
└─────────────┘     └─────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does a generic repository mean you never need custom data methods? Commit yes or no.
Common Belief:A generic repository handles all data needs, so you don't need any custom methods.
Tap to reveal reality
Reality:Generic repositories cover common CRUD operations, but specific entities often need custom queries or logic, requiring extended repositories.
Why it matters:Ignoring the need for custom methods leads to forcing complex queries into generic methods, causing messy and inefficient code.
Quick: Is using a generic repository always better than writing specific repositories? Commit yes or no.
Common Belief:Generic repositories are always the best choice for data access.
Tap to reveal reality
Reality:While generic repositories reduce duplication, they can add unnecessary abstraction for simple apps or when data access is very different per entity.
Why it matters:Overusing this pattern can make code harder to understand and maintain if the abstraction doesn't fit the problem.
Quick: Does the generic repository pattern force you to use in-memory storage? Commit yes or no.
Common Belief:Generic repositories only work with in-memory data like arrays.
Tap to reveal reality
Reality:Generic repositories can be implemented with any storage, including databases, APIs, or files, as long as they follow the interface.
Why it matters:Believing this limits the pattern's usefulness and prevents applying it to real-world data sources.
Quick: Can you use generic repositories without TypeScript generics? Commit yes or no.
Common Belief:You can write generic repositories without using generics, just by using any type.
Tap to reveal reality
Reality:Without generics, you lose type safety and flexibility, making the repository less reliable and harder to maintain.
Why it matters:Ignoring generics leads to bugs and defeats the purpose of the pattern.
Expert Zone
1
Generic repositories often need to balance between too generic (losing specific features) and too specific (losing reuse), which requires careful design.
2
When integrating with ORMs, generic repositories can sometimes duplicate ORM features, so understanding when to wrap or extend ORM methods is crucial.
3
TypeScript's structural typing means entities don't need to explicitly implement interfaces to work with generic repositories, but this can cause subtle bugs if entity shapes change unexpectedly.
When NOT to use
Avoid generic repositories when your data access logic is highly specialized per entity or when using advanced ORM features that already provide rich repositories. In such cases, direct ORM repositories or query builders are better. Also, for very simple apps, the added abstraction may be unnecessary.
Production Patterns
In production, generic repositories are often combined with dependency injection frameworks to swap implementations (e.g., mock vs real database). They are extended for domain-specific queries and used alongside unit of work patterns to manage transactions. Logging, caching, and error handling are also integrated at the repository level.
Connections
Dependency Injection
Builds-on
Understanding dependency injection helps you see how generic repositories can be provided and swapped easily, improving modularity and testability.
Object-Oriented Programming (OOP) Interfaces
Same pattern
Generic repositories rely on interfaces to define contracts, showing how OOP principles enforce consistent behavior across different implementations.
Supply Chain Management
Analogous process
Just like a warehouse manages different products with standard processes (receiving, storing, shipping), a generic repository manages different data entities with standard operations, highlighting universal management principles.
Common Pitfalls
#1Trying to store entities without an id property in the generic repository.
Wrong approach:class GenericRepository { private data: T[] = []; add(item: T) { this.data.push(item); } get(id: string): T | undefined { return this.data.find(item => (item as any).id === id); } }
Correct approach:interface IEntity { id: string; } class GenericRepository { private data: T[] = []; add(item: T) { this.data.push(item); } get(id: string): T | undefined { return this.data.find(item => item.id === id); } }
Root cause:Not constraining the generic type to entities with an id causes runtime errors and unsafe code.
#2Writing separate repository classes for each entity with duplicated CRUD code.
Wrong approach:class UserRepository { private users: User[] = []; add(user: User) { this.users.push(user); } get(id: string) { return this.users.find(u => u.id === id); } // same methods repeated for ProductRepository }
Correct approach:class GenericRepository { private data: T[] = []; add(item: T) { this.data.push(item); } get(id: string) { return this.data.find(i => i.id === id); } } class UserRepository extends GenericRepository {}
Root cause:Not using generics leads to repetitive code and harder maintenance.
#3Adding complex query logic inside the generic repository class.
Wrong approach:class GenericRepository { // ... CRUD methods findByEmail(email: string) { /* only valid for User */ } }
Correct approach:class UserRepository extends GenericRepository { findByEmail(email: string) { return this.data.find(u => u.email === email); } }
Root cause:Mixing generic and entity-specific logic breaks abstraction and reduces code clarity.
Key Takeaways
The generic repository pattern uses TypeScript generics to create reusable data access code for any entity type.
It separates data operations from business logic, improving code organization and maintainability.
Constraints on generics ensure entities have required properties like an id for safe operations.
Extending generic repositories allows adding custom methods for specific data needs without duplicating code.
Combining this pattern with dependency injection enhances flexibility, testability, and scalability in real-world applications.