Generics are a way to create functions, classes, and interfaces that can work with multiple types. They introduce type variables, which are placeholders for actual types. These type variables are specified when the generic component is used.
Let’s start with a simple example of a generic function that returns the same value it receives.
function identity<T>(arg: T): T {
return arg;
}
// Using the generic function with a number
let output1 = identity<number>(10);
// Using the generic function with a string
let output2 = identity<string>("Hello");
console.log(output1);
console.log(output2);
In the above code, <T>
is the type variable. It represents a type that will be determined when the function is called. When we call identity<number>(10)
, T
is replaced with number
. Similarly, when we call identity<string>("Hello")
, T
is replaced with string
.
We can also define generic types. For example, we can define a generic type for the identity
function we created earlier.
type IdentityFunction<T> = (arg: T) => T;
let myIdentity: IdentityFunction<number> = identity;
let result = myIdentity(20);
console.log(result);
Here, IdentityFunction<T>
is a generic type that represents a function that takes an argument of type T
and returns a value of type T
.
Generic classes are similar to generic functions. They have a generic type parameter list in angle brackets (<>
) following the class name.
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
constructor(zeroValue: T, addFunction: (x: T, y: T) => T) {
this.zeroValue = zeroValue;
this.add = addFunction;
}
}
// Using the generic class with numbers
let myNumber = new GenericNumber<number>(0, (x, y) => x + y);
let sum = myNumber.add(5, 10);
console.log(sum);
// Using the generic class with strings
let myString = new GenericNumber<string>("", (x, y) => x + y);
let concatenated = myString.add("Hello ", "World");
console.log(concatenated);
In this example, the GenericNumber
class can work with different types. We can use it with numbers to perform addition or with strings to perform concatenation.
Sometimes, we want to limit the types that a generic type variable can accept. We can do this using generic constraints.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
// This works because a string has a length property
loggingIdentity("Hello");
// This would cause a compilation error because a number does not have a length property
// loggingIdentity(10);
Here, the T extends Lengthwise
constraint ensures that the type T
must have a length
property.
Generics are commonly used with arrays. TypeScript has a built - in generic type for arrays, Array<T>
.
let numbers: Array<number> = [1, 2, 3, 4, 5];
let strings: Array<string> = ["apple", "banana", "cherry"];
function printArray<T>(arr: Array<T>): void {
for (let item of arr) {
console.log(item);
}
}
printArray(numbers);
printArray(strings);
This allows us to create arrays of different types and use a single function to print their elements.
When working with APIs, we can use generic interfaces to handle different types of responses.
interface ApiResponse<T> {
success: boolean;
data: T;
message: string;
}
// API response with a number
let numberResponse: ApiResponse<number> = {
success: true,
data: 100,
message: "Successfully retrieved data"
};
// API response with an object
let objectResponse: ApiResponse<{ name: string, age: number }> = {
success: true,
data: { name: "John", age: 30 },
message: "User data retrieved"
};
This way, we can handle different types of API responses in a type - safe manner.
Use meaningful names for type variables. For example, instead of using T
, use Item
if the generic is related to items in a collection.
function getFirstElement<Item>(arr: Array<Item>): Item | undefined {
return arr.length > 0 ? arr[0] : undefined;
}
This makes the code more readable and easier to understand.
Don’t use generic type variables where they are not needed. Only introduce a generic type variable when it is necessary to make the code reusable with different types.
Use generic constraints to ensure that the generic type variable meets certain requirements. This helps to catch type - related errors at compile time.
Generics in TypeScript are a powerful feature that allows us to create reusable and type - safe code. They enable us to write functions, classes, and interfaces that can work with multiple types. By understanding the fundamental concepts, usage methods, common practices, and best practices of generics, developers can write more maintainable and robust TypeScript code. Whether it’s working with arrays, handling API responses, or creating generic classes, generics provide a flexible and efficient way to deal with different data types.