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
- Multiple Type Parameters
- Generic Constraints
- Generic Class Methods
- Practical Example: Generic Data Repository
- Best Practices for Generic Classes
- Error Handling in Generic Classes
- Integrating with TypeScript Interfaces
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.