TypeScript Generic Classes: The Complete Guide for Beginners

Generic classes in TypeScript provide a powerful way to create reusable, type-safe components that can work with different data types. Whether you’re building collections, data structures, or service classes, understanding generic classes will make your TypeScript code more flexible and maintainable.

Let’s explore how to master generic classes in TypeScript, from basic concepts to practical implementations.

Table of Contents

Understanding Generic Classes

A generic class allows you to create a class that can work with different types while maintaining type safety. Instead of committing to a specific type, you use a type parameter that gets specified when the class is used.

Basic Syntax

class Container<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }

  setValue(value: T): void {
    this.value = value;
  }
}
Code language: JavaScript (javascript)

Using Generic Classes

// String container
const stringContainer = new Container<string>('Hello World');
const str = stringContainer.getValue(); // Type is string

// Number container
const numberContainer = new Container<number>(42);
const num = numberContainer.getValue(); // Type is number
Code language: JavaScript (javascript)

Multiple Type Parameters

Generic classes can use multiple type parameters when you need to handle different types within the same class.

class KeyValuePair<K, V> {
  constructor(private key: K, private value: V) {}

  getKey(): K {
    return this.key;
  }

  getValue(): V {
    return this.value;
  }
}

const pair = new KeyValuePair<string, number>('age', 25);
console.log(pair.getKey()); // 'age'
console.log(pair.getValue()); // 25
Code language: JavaScript (javascript)

Generic Constraints

Sometimes you want to limit what types can be used with your generic class. Generic constraints allow you to specify requirements for the type parameters.

interface HasLength {
  length: number;
}

class LengthChecker<T extends HasLength> {
  constructor(private value: T) {}

  getLength(): number {
    return this.value.length;
  }
}

// Valid - string has a length property
const stringChecker = new LengthChecker('hello');
console.log(stringChecker.getLength()); // 5

// Valid - array has a length property
const arrayChecker = new LengthChecker([1, 2, 3]);
console.log(arrayChecker.getLength()); // 3

// Error - number doesn't have a length property
// const numberChecker = new LengthChecker(42);
Code language: JavaScript (javascript)

Generic Class Methods

In addition to class-level type parameters, you can also have generic methods within your classes.

class Utilities<T> {
  private items: T[] = [];

  addItem(item: T): void {
    this.items.push(item);
  }

  // Generic method with its own type parameter
  transform<U>(item: T, transformer: (value: T) => U): U {
    return transformer(item);
  }
}

const utils = new Utilities<number>();
utils.addItem(42);

const result = utils.transform(42, (n) => n.toString());
console.log(typeof result); // 'string'
Code language: JavaScript (javascript)

Practical Example: Generic Data Repository

Let’s create a practical example of a generic repository class that could be used for data access:

interface Entity {
  id: number;
}

class Repository<T extends Entity> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  update(item: T): void {
    const index = this.items.findIndex(i => i.id === item.id);
    if (index !== -1) {
      this.items[index] = item;
    }
  }

  delete(id: number): void {
    this.items = this.items.filter(item => item.id !== id);
  }

  find(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  getAll(): T[] {
    return [...this.items];
  }
}

// Example usage
interface User extends Entity {
  name: string;
  email: string;
}

const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: 'John', email: '[email protected]' });

const user = userRepo.find(1);
console.log(user?.name); // 'John'
Code language: JavaScript (javascript)

Best Practices for Generic Classes

Use Descriptive Type Parameter Names

    • Use T for general types
    • Use K for keys
    • Use V for values
    • Use more descriptive names for specific use cases

    Provide Default Type Parameters

      class DefaultConfig<T = string> {
        constructor(private value: T) {}
      
        getValue(): T {
          return this.value;
        }
      }
      
      // No type parameter needed for string
      const config = new DefaultConfig('default');
      Code language: JavaScript (javascript)

      Use Constraints Appropriately

        Constraints should be used to ensure type safety and provide access to required properties or methods:

        interface Identifiable {
          id: string | number;
        }
        
        class Database<T extends Identifiable> {
          find(id: T['id']): T | undefined {
            // Implementation
            return undefined;
          }
        }
        Code language: JavaScript (javascript)

        Error Handling in Generic Classes

        When working with generic classes, it’s important to handle errors appropriately:

        class Result<T, E extends Error> {
          private constructor(
            private value: T | null,
            private error: E | null
          ) {}
        
          static success<T>(value: T): Result<T, never> {
            return new Result(value, null);
          }
        
          static failure<E extends Error>(error: E): Result<never, E> {
            return new Result(null, error);
          }
        
          isSuccess(): boolean {
            return this.error === null;
          }
        
          getValue(): T {
            if (this.value === null) throw this.error;
            return this.value;
          }
        }
        Code language: JavaScript (javascript)

        Integrating with TypeScript Interfaces

        Generic classes work well with interfaces to create flexible, type-safe abstractions:

        interface DataProvider<T> {
          getData(): Promise<T>;
        }
        
        class ApiService<T> implements DataProvider<T> {
          constructor(private url: string) {}
        
          async getData(): Promise<T> {
            const response = await fetch(this.url);
            return response.json();
          }
        }
        Code language: JavaScript (javascript)

        By mastering generic classes in TypeScript, you can create more reusable and type-safe code. They provide a powerful way to build flexible components while maintaining strong typing throughout your application.

        Remember to start simple and gradually incorporate more advanced features as needed. The goal is to create maintainable code that leverages TypeScript’s type system effectively.

        For more insights into TypeScript’s type system, check out our guide on TypeScript Type Guards which pairs well with generic classes for runtime type checking.

        Leave a Comment

        This site uses Akismet to reduce spam. Learn how your comment data is processed.

        Share via
        Copy link
        Powered by Social Snap