In our journey through TypeScript generics, we’ve learned how to use them with functions. But generics don’t stop there; they extend their power to interfaces and classes, enabling us to create flexible and reusable data structures with impeccable type safety. In this chapter, we’ll explore how generics can be applied to interfaces and classes and showcase their real-world applications.
Applying Generics to Interfaces
Interfaces in TypeScript define the structure of an object, specifying the shape of its properties and their types. With generics, we can create interfaces that work with a variety of data types while maintaining type safety.
Let’s start with a simple example by creating a generic interface for key-value pairs:
interface KeyValuePair<K, V> { key: K; value: V; } const pair1: KeyValuePair<number, string> = { key: 1, value: "one" }; const pair2: KeyValuePair<string, boolean> = { key: "enabled", value: true };
In this example, KeyValuePair
is a generic interface with two type parameters K
and V
. It can represent key-value pairs of different data types. The flexibility of this generic interface allows us to work with various combinations of key and value types.
Another practical example:
Suppose you want to create a generic interface to represent a collection of items with varying types. You can do so like this:
interface Collection<T> { items: T[]; add(item: T): void; remove(item: T): void; } // Example usage const numberCollection: Collection<number> = { items: [1, 2, 3], add(item) { this.items.push(item); }, remove(item) { const index = this.items.indexOf(item); if (index !== -1) { this.items.splice(index, 1); } }, }; numberCollection.add(4); numberCollection.remove(2);
The Collection
interface is generic, with a type parameter T
. It defines three members:
items: T[]
: This member represents an array of items of typeT
. It’s used to store the collection’s elements.add(item: T): void
: This method takes an argument of typeT
and is responsible for adding an item to the collection.remove(item: T): void
: This method takes an argument of typeT
and is responsible for removing an item from the collection.
- In this example, a
numberCollection
object is created. It’s of typeCollection<number
, which means it’s a collection of numbers. - The
items
property is initialized with an array[1, 2, 3]
. This array stores the numbers in the collection. - The
add
method is defined as a function that takes an argumentitem
, which is expected to be a number. It usesthis.items.push(item)
to add the specified number to the collection. - The
remove
method is defined as a function that takes an argumentitem
, which is expected to be a number. It usesthis.items.indexOf(item)
to find the index of the specified number in the collection. If the number is found (index is not -1), it usesthis.items.splice(index, 1)
to remove the number from the collection. - The
numberCollection
object is now an instance of theCollection<number>
interface, which means it must adhere to the interface’s structure and type constraints. It can be used to add and remove numbers from the collection.
Creating Generic Classes
Just like interfaces, classes in TypeScript can also benefit from generics. With generic classes, you can create reusable data structures and components. Let’s look at a practical example:
class Box<T> { private value: T; constructor(initialValue: T) { this.value = initialValue; } getValue(): T { return this.value; } setValue(newValue: T): void { this.value = newValue; } }
Class Definition:
class Box<T>
: This line defines a TypeScript class calledBox
. The<T>
in the class definition indicates thatBox
is a generic class with a type parameterT
.T
is a placeholder for the data type that will be specified when creating instances of the class.
Class Members:
private value: T;
: This line declares a private member variablevalue
of typeT
. It’s where the class stores the value of the specified data type.
Constructor:
constructor(initialValue: T)
: This is the class constructor. It takes an initial value of typeT
as a parameter. When you create an instance of theBox
class, you must provide an initial value of the specific data type.this.value = initialValue;
: Inside the constructor, thevalue
member variable is initialized with the providedinitialValue
. The value’s data type is inferred from the type parameterT
.
Methods:
getValue(): T
: This method returns the stored value. It specifies that the return type is of the same typeT
as the stored value.setValue(newValue: T): void
: This method allows you to set a new value of typeT
. It takes the new value as a parameter.
Usage:
const numberBox = new Box<number>(42);
: Here, we create an instance of theBox
class for numbers. We specify<number>
in the angle brackets to indicate that this instance will work with numbers. We provide an initial value of42
.console.log(numberBox.getValue());
: We call thegetValue
method to retrieve the stored value, which is42
.const stringBox = new Box<string>("Hello, TypeScript");
: Similarly, we create another instance of theBox
class, this time for strings. We specify<string>
and provide an initial value of"Hello, TypeScript"
.console.log(stringBox.getValue());
: We call thegetValue
method to retrieve the stored string value.
In this example, the generic class Box
allows you to store and retrieve values of different data types while maintaining type safety. The type parameter T
ensures that the stored value and the retrieved value are of the same data type.