TypeScript, with its powerful type system, offers a feature called Generics, which enables developers to write reusable and type-safe code. Generics allow you to create components that can work over a variety of types rather than a single one.
This article delves into TypeScript Generics, providing thorough explanations and code examples to illustrate their usage and benefits.
You can get all the source code from here.
Table of Contents
What are Generics?
Generics in TypeScript enable writing code that can work with a variety of data types while maintaining type safety. They allow the creation of reusable components, functions, and data structures without sacrificing type checking.
Generics are represented by type parameters, which act as placeholders for types. These parameters are specified within angle brackets (<>
) and can be used throughout the code to define types of variables, function parameters, return types, and more.
TypeScript Generics Use Cases
Basic Usage of Generics
Let's start with a simple example of a generic function:
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("hello");
console.log(output); // Output: hello
In this example, identity
is a generic function that takes a type parameter T
. The parameter arg
is of type T
, and the return type of the function is also T
. When calling identity<string>("hello")
, the type parameter T
is inferred as string
, ensuring type safety.
How to Use Generic Classes
Generics are not limited to functions – they can also be used with classes. Consider the following example of a generic Box
class:
class Box<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
let box = new Box<number>(42);
console.log(box.getValue()); // Output: 42
Here, Box
is a generic class with a type parameter T
. The constructor takes a value of type T
, and the getValue
method returns a value of type T
. When creating an instance of Box<number>
, it can only store and return values of type number
.
How to Apply Constraints on Generics
Sometimes, you may want to restrict the types that can be used with generics. TypeScript allows you to specify constraints on type parameters using the extends
keyword. Let's see an example:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
let result = loggingIdentity("hello");
console.log(result); // Output: hello
In this example, the loggingIdentity
function takes a type parameter T
that must extend the Lengthwise
interface, which ensures that arg
has a length
property. This constraint allows accessing the length
property without causing a compilation error.
How to Use Generics with Interfaces
Generics can also be used with interfaces to create flexible and reusable definitions. Consider the following example:
interface Pair<T, U> {
first: T;
second: U;
}
let pair: Pair<number, string> = { first: 1, second: "two" };
console.log(pair); // Output: { first: 1, second: "two" }
Here, Pair
is an interface with two type parameters T
and U
, representing the types of the first
and second
properties respectively. When declaring pair
as Pair<number, string>
, it enforces that the first
property must be a number, and the second
property must be a string.
How to Use Generic Functions with an Array
function reverse<T>(array: T[]): T[] {
return array.reverse();
}
let numbers: number[] = [1, 2, 3, 4, 5];
let reversedNumbers: number[] = reverse(numbers);
console.log(reversedNumbers); // Output: [5, 4, 3, 2, 1]
In this example, the reverse
function takes an array of type T
and returns a reversed array of the same type. By using generics, the function can work with arrays of any type, ensuring type safety.
How to Use Generic Constraints with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let person = { name: "John", age: 30, city: "New York" };
let age: number = getProperty(person, "age");
console.log(age); // Output: 30
Here, the getProperty
function takes an object of type T
and a key of type K
, where K
extends the keys of T
. It then returns the corresponding property value from the object. This example demonstrates how to use generics with keyof
to enforce type safety when accessing object properties.
How to Use Generic Utility Functions
function toArray<T>(value: T): T[] {
return [value];
}
let numberArray: number[] = toArray(42);
console.log(numberArray); // Output: [42]
let stringArray: string[] = toArray("hello");
console.log(stringArray); // Output: ["hello"]
The toArray
function converts a single value of type T
into an array containing that value. This simple utility function showcases how generics can be used to create reusable code that adapts to different data types effortlessly.
How to Use Generic Interfaces with A Function
interface Transformer<T, U> {
(input: T): U;
}
function uppercase(input: string): string {
return input.toUpperCase();
}
let transform: Transformer<string, string> = uppercase;
console.log(transform("hello")); // Output: HELLO
In this example, we define a Transformer
interface with two type parameters T
and U
, representing the input and output types respectively. We then declare a function uppercase
and assign it to a variable transform
of type Transformer<string, string>
. This demonstrates how generics can be used to define flexible interfaces for functions.
Conclusion
Whether it's functions, classes, or interfaces, generics provide a robust mechanism for building scalable and maintainable TypeScript applications. Understanding and mastering generics can significantly enhance your ability to write efficient and error-free code.