Featured resource
2025 Tech Upskilling Playbook
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Check it out
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Guided: Build Reusable Components with TypeScript Generics

Learn to use TypeScript generics to build a reusable, type-safe data layer for a RESTful API. In this lab, you'll implement a generic repository pattern to handle multiple entity types with minimal code duplication, then expose these operations via Express endpoints. By the end, you'll understand how generics and type constraints enable scalable, maintainable backend design.

Lab platform
Lab Info
Level
Beginner
Last updated
Sep 19, 2025
Duration
41m

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.
Table of Contents
  1. Challenge

    Generic Syntax

    Understanding TypeScript's Generic Syntax

    Generics are a powerful feature in TypeScript that allows you to write flexible, reusable code without losing type safety. In this section, you'll learn how to define and use generics effectively.

    Declaring Generics

    Generics are declared using angle brackets, such as <T>, where T is a placeholder for a type. You can use generics with functions, classes, or interfaces to make them work with any data type.

    Example: Generic Function

    Here's a simple example of a generic function:

    function identity<T>(arg: T): T {
        return arg;
    }
    
    // explicitly specifying the type
    const result1 = identity<string>("hello");
    // or let TypeScript infer it
    const result2 = identity(42);
    
    • identity<T> declares a generic function with a type parameter T.
    • When calling identity, you can specify the type explicitly for example, identity<string>("hello").
    • Alternatively, you can let TypeScript infer the type from the argument.

    With this approach, you can write functions that support any type. TypeScript maintains type safety and automatically infers types for you.


    Next, you'll use these concepts to review your repository patterns and plan how to introduce generics for greater flexibility and maintainability.

  2. Challenge

    Why use Generics?

    Why Use Generics?

    Generics in TypeScript allow you to write reusable, type-safe code that works with any data type. By abstracting over types, you eliminate duplicated logic, simplify maintenance, and ensure consistency—all while preserving strong compile-time checks.

    The Problem

    To see the problem, consider the following two repository classes: CustomerRepository.ts and ProductRepository.ts. Both classes implement similar CRUD operations but handle different data types.

    Note: You can view and edit these files in the /repositories directory located in the file explorer on the right.

    When you duplicate logic across files, it increases the chance of inconsistency, makes maintenance harder, and raises the risk of errors—especially when you need to make updates in multiple places.

    The Solution: Generics

    Generics allow you to create a single implementation that works with any type. By introducing a generic type parameter, you eliminate duplication and improve type safety because TypeScript catches type errors at compile time.


    Next, apply these concepts to improve your repository patterns by introducing generics for better flexibility and maintainability.

  3. Challenge

    Refactoring IRepository

    Refactoring IRepository for Stronger Typing

    To make your repositories more flexible and type-safe, define a generic interface that can handle any entity type. This allows repository classes to share common CRUD operations without losing the benefits of TypeScript’s strong typing.

    Currently, both CustomerRepository and ProductRepository implement the same set of CRUD methods, which are declared in the IRepository.ts interface. However, this interface uses the any type for its operations, which undermines type safety.

    When you refactor the interface to use generics, you ensure that each repository works only with its specific entity type—reducing errors and improving code quality.

    Next you'll create the shared functionality in a BaseRepository.ts file that the repository files can extend.

  4. Challenge

    Designing a Reusable BaseRepository

    Designing a Reusable BaseRepository

    After defining a generic repository interface, you'll implement a base class that provides type-safe CRUD operations for any entity type. This reusable class becomes the foundation of your repositories, allowing you to centralize common logic and reduce code duplication.

    You can then create specific repositories by extending this base class for each entity type. Once you’ve declared the class, implement the CRUD methods.

    You can reuse the logic from an existing repository (for example, CustomerRepository or ProductRepository) and update it to work with generics.

  5. Challenge

    Restricting Generic Types

    Restricting Generic Types

    When you defined the BaseRepository class, you used a generic type T. However, TypeScript could not guarantee that T included an id property. To fix this, you should restrict T so it must extend a base entity type that defines the id property.

    This change ensures that any type used with BaseRepository will always have the required structure.

    There are two common ways to enforce this constrain.

    Using a Base Entity Class:
    • Create a base class (such as BaseEntity) with the id property, and require that T extends this class:
    BaseRepository<T extends BaseEntity>
    
    Using a Generic Constraint:
    • Directly constrain T to types that have an id property:
    BaseRepository<T extends { id: number }>
    

    In the /models directory, you'll find a BaseEntity.ts file. Each entity (such as Customer) already implements BaseEntity, so they all include the required id property.

    Now that BaseRepository enforces the BaseEntity constraint, you can confidently create specialized repositories for your entities. This approach guarantees type safety and consistency throughout your application.

  6. Challenge

    Refactoring to Generic Type

    Refactoring to Generic Type

    Now you can simplify your repositories by having them extend the generic BaseRepository class. This approach will eliminate duplicated code while preserving any specific functionality unique to each repository.

    Congratulations! You have successfully refactored your repositories to use a generic base class. This approach not only reduces code duplication but also enhances maintainability and readability of your codebase.

About the author

Jeff Hopper is a polyglot solution developer with over 20 years of experience across several business domains. He has enjoyed many of those years focusing on the .Net stack.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight