Table of Contents
- Introduction
- What Are Generic Interfaces?
- Defining a Generic Interface
- Using Generic Interfaces with Different Types
- Generic Classes in TypeScript
- Defining a Generic Class
- Using Generic Classes with Multiple Types
- Constraints in Generic Interfaces and Classes
- Example 1: A Generic Container Class
- Example 2: A Generic Logging Interface
- Best Practices for Using Generic Interfaces and Classes
- Conclusion
Introduction
In TypeScript, generics are not limited to just functions. You can also define generic interfaces and generic classes, which enable you to write flexible, reusable, and type-safe code for a wide range of data structures and object types. Both generic interfaces and classes allow you to define templates for data types that can be reused throughout your code, offering strong typing and preventing common runtime errors.
In this article, we will dive into generic interfaces and generic classes, explore their syntax and usage, and showcase practical examples to help you understand how they work.
What Are Generic Interfaces?
A generic interface is an interface that can take a type parameter, allowing the interface to define a contract for various types. When you use a generic interface, you can specify the type that it should work with, making the interface flexible while still retaining type safety.
Generic interfaces are commonly used when defining collections or structures that need to work with different data types, like arrays, linked lists, or containers.
Defining a Generic Interface
To define a generic interface in TypeScript, you use angle brackets (<T>) to specify a type parameter inside the interface declaration. This type parameter will be used to define the types of the properties or methods in the interface.
Basic Syntax
interface Box<T> {
value: T;
getValue: () => T;
}
In this example:
- The
Boxinterface has a type parameterT, which represents the type of thevalueproperty. - The
getValuemethod also returns a value of typeT.
Using the Generic Interface
When you create an object based on the Box interface, you can specify the type for T. TypeScript will then infer and enforce the type for the properties and methods accordingly.
const numberBox: Box<number> = {
value: 42,
getValue: function () {
return this.value;
},
};
const stringBox: Box<string> = {
value: 'Hello, TypeScript!',
getValue: function () {
return this.value;
},
};
Here, numberBox is a Box that holds a number, and stringBox is a Box that holds a string.
Using Generic Interfaces with Different Types
You can also define more complex generic interfaces that work with multiple types. TypeScript allows you to define multiple type parameters in a generic interface.
Example: Pairing Two Values
interface Pair<T, U> {
first: T;
second: U;
}
const numberStringPair: Pair<number, string> = {
first: 1,
second: 'apple',
};
const stringBooleanPair: Pair<string, boolean> = {
first: 'isActive',
second: true,
};
In this example:
- The
Pairinterface has two type parameters,TandU. numberStringPairholds anumberand astring.stringBooleanPairholds astringand aboolean.
Generic Classes in TypeScript
A generic class is a class that can work with multiple types by defining a type parameter. This is useful when you want to create a class that works with different types of data, but still wants to ensure that the types are correct.
Just like with interfaces, you can define a generic class using the angle bracket syntax <T>, and then use T within the class to define the types of properties or methods.
Defining a Generic Class
Here’s the syntax for a basic generic class:
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
In this example:
Containeris a generic class that takes a type parameterT.- The class has a private property
valueof typeT, and a methodgetValuethat returns the value of typeT.
Using Generic Classes with Multiple Types
You can also define a class that works with multiple type parameters, allowing you to create more complex data structures.
Example: A Generic Key-Value Store
class KeyValueStore<K, V> {
private store: Map<K, V>;
constructor() {
this.store = new Map();
}
set(key: K, value: V): void {
this.store.set(key, value);
}
get(key: K): V | undefined {
return this.store.get(key);
}
}
const stringNumberStore = new KeyValueStore<string, number>();
stringNumberStore.set('age', 25);
console.log(stringNumberStore.get('age')); // Output: 25
const numberStringStore = new KeyValueStore<number, string>();
numberStringStore.set(1, 'apple');
console.log(numberStringStore.get(1)); // Output: 'apple'
In this example:
- The
KeyValueStoreclass has two type parameters,KandV, which represent the key and value types respectively. - We can create a
KeyValueStorethat stores string keys and number values, or one that stores number keys and string values.
Constraints in Generic Interfaces and Classes
Just like generic functions, generic interfaces and classes can also have constraints. Constraints ensure that the type used in the generic type parameter adheres to a certain shape, allowing you to restrict the types that can be used.
Example: Constrained Generic Interface
interface Lengthwise {
length: number;
}
interface Container<T extends Lengthwise> {
value: T;
getLength(): number;
}
const stringContainer: Container<string> = {
value: 'Hello',
getLength() {
return this.value.length;
},
};
const arrayContainer: Container<number[]> = {
value: [1, 2, 3],
getLength() {
return this.value.length;
},
};
// const numberContainer: Container<number> // Error: number doesn't have a length property
In this example:
- The
Containerinterface has a constraint onT(T extends Lengthwise), ensuring that only types with alengthproperty (likestringorArray) can be used.
Example: Constrained Generic Class
class Box<T extends { length: number }> {
private value: T;
constructor(value: T) {
this.value = value;
}
getLength(): number {
return this.value.length;
}
}
const boxWithArray = new Box([1, 2, 3]); // Valid
const boxWithString = new Box('Hello'); // Valid
// const boxWithNumber = new Box(42); // Error: number doesn't have a length property
Here:
- The
Boxclass has a constraint onTthat ensures it can only be used with types that have alengthproperty.
Example 1: A Generic Container Class
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
setValue(value: T): void {
this.value = value;
}
}
const numberContainer = new Container<number>(100);
console.log(numberContainer.getValue()); // Output: 100
numberContainer.setValue(200);
console.log(numberContainer.getValue()); // Output: 200
Example 2: A Generic Logging Interface
interface Logger<T> {
log(value: T): void;
}
class ConsoleLogger<T> implements Logger<T> {
log(value: T): void {
console.log(value);
}
}
const stringLogger = new ConsoleLogger<string>();
stringLogger.log('Hello, World!'); // Output: Hello, World!
const numberLogger = new ConsoleLogger<number>();
numberLogger.log(42); // Output: 42
Best Practices for Using Generic Interfaces and Classes
- Be Descriptive with Type Parameters: Use meaningful names for type parameters (e.g.,
TbecomesItemTypeorKeyType) to improve code readability. - Use Constraints: Use constraints to limit the types that can be used with generics when necessary. This ensures type safety while maintaining flexibility.
- Avoid Overuse of Generics: While generics are powerful, excessive use can make code harder to understand. Use generics when they make your code more reusable and flexible, but avoid overcomplicating it.
- Keep Generic Methods Simple: If you’re writing methods within generic classes or interfaces, keep them simple and focused on the specific functionality needed.
Conclusion
Generic interfaces and classes in TypeScript provide a powerful way to create flexible, reusable, and type-safe code that works with various types of data. By using generics, you can ensure that your classes and interfaces can adapt to different data types while still retaining the benefits of type safety.
With these tools, you can write more maintainable and modular code that will scale as your application grows.

