Bird
Raised Fist0
Typescriptprogramming~15 mins

Generic repository pattern in Typescript - Deep Dive

Choose your learning style10 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.

Practice

(1/5)
1.

What is the main benefit of using the Generic Repository Pattern in TypeScript?

easy
A. It allows handling multiple data types with one reusable class.
B. It automatically generates UI components.
C. It replaces the need for TypeScript interfaces.
D. It compiles TypeScript code faster.

Solution

  1. Step 1: Understand the purpose of the generic repository

    The generic repository pattern is designed to handle data operations for many types using one class or interface.
  2. Step 2: Compare options with the pattern's purpose

    Only It allows handling multiple data types with one reusable class. correctly describes this benefit. Options A, B, and C are unrelated to the pattern's main use.
  3. Final Answer:

    It allows handling multiple data types with one reusable class. -> Option A
  4. Quick Check:

    Generic repository = reusable data handler [OK]
Hint: Generic means reusable for many types [OK]
Common Mistakes:
  • Thinking it generates UI components
  • Confusing it with TypeScript compilation
  • Believing it removes interfaces
2.

Which of the following is the correct way to declare a generic repository interface in TypeScript?

interface IRepository<T> {
  add(item: T): void;
  get(id: number): T | undefined;
}
easy
A. interface IRepository<T> { add(item: T): void; get(id: number): T | undefined; }
B. interface IRepository { add(item: any): void; get(id: number): any; }
C. interface IRepository<T> { add(item: any): void; get(id: number): T; }
D. interface IRepository<T> { add(item: T): T; get(id: string): T | undefined; }

Solution

  1. Step 1: Check generic syntax correctness

    interface IRepository<T> { add(item: T): void; get(id: number): T | undefined; } correctly declares a generic interface with type parameter <T> and uses it consistently in method signatures.
  2. Step 2: Validate method signatures

    interface IRepository<T> { add(item: T): void; get(id: number): T | undefined; }'s methods use item: T and return T | undefined for get, which is correct. Other options misuse types or parameter types.
  3. Final Answer:

    interface IRepository<T> { add(item: T): void; get(id: number): T | undefined; } -> Option A
  4. Quick Check:

    Generic interface syntax = interface IRepository<T> { add(item: T): void; get(id: number): T | undefined; } [OK]
Hint: Generic interfaces use <T> and consistent types [OK]
Common Mistakes:
  • Using 'any' instead of generic type T
  • Mismatching parameter and return types
  • Using wrong id parameter type
3.

Given the following generic repository class, what will be the output of the console.log statement?

class Repository<T> {
  private items: T[] = [];
  add(item: T): void {
    this.items.push(item);
  }
  getAll(): T[] {
    return this.items;
  }
}

const repo = new Repository<string>();
repo.add('apple');
repo.add('banana');
console.log(repo.getAll());
medium
A. Error: Type mismatch
B. ['apple']
C. []
D. ['apple', 'banana']

Solution

  1. Step 1: Understand the generic repository usage

    The repository is created for strings, so it stores string items in an array.
  2. Step 2: Trace the add and getAll methods

    Two strings 'apple' and 'banana' are added, so getAll returns ['apple', 'banana'].
  3. Final Answer:

    ['apple', 'banana'] -> Option D
  4. Quick Check:

    Added items = ['apple', 'banana'] [OK]
Hint: Add items then getAll returns all added [OK]
Common Mistakes:
  • Expecting only one item in output
  • Thinking getAll returns empty array
  • Confusing type errors
4.

Identify the error in this generic repository implementation:

class Repository<T> {
  private items: T[] = [];
  add(item: T): void {
    this.items.push(item);
  }
  get(id: number): T {
    return this.items[id];
  }
}

const repo = new Repository<number>();
console.log(repo.get(0));
medium
A. The items array should be public, not private.
B. The add method should return the added item.
C. The get method should return T | undefined to handle missing items.
D. The generic type T cannot be used with arrays.

Solution

  1. Step 1: Analyze the get method return type

    The get method returns this.items[id], which may be undefined if the index is invalid.
  2. Step 2: Check return type compatibility

    Since undefined is possible, the return type should be T | undefined to avoid runtime errors.
  3. Final Answer:

    The get method should return T | undefined to handle missing items. -> Option C
  4. Quick Check:

    Return type must allow undefined for missing index [OK]
Hint: Allow undefined return for invalid index [OK]
Common Mistakes:
  • Ignoring possible undefined return
  • Changing add method unnecessarily
  • Making items public without reason
5.

You want to create a generic repository that supports updating an item by id. Which method signature correctly fits this requirement?

hard
A. update(item: T): void
B. update(id: number, item: T): boolean
C. update(id: string, item: any): T
D. update(id: number): T | undefined

Solution

  1. Step 1: Understand update method needs

    To update an item, you need to specify which item by id and provide the new item data.
  2. Step 2: Evaluate method signatures

    update(id: number, item: T): boolean takes an id and a new item of type T, returning boolean to indicate success, which fits the requirement best.
  3. Final Answer:

    update(id: number, item: T): boolean -> Option B
  4. Quick Check:

    Update needs id and new item, returns success [OK]
Hint: Update needs id and new item, returns success boolean [OK]
Common Mistakes:
  • Missing id parameter
  • Using wrong parameter types
  • Returning wrong type from update
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard