TypeScript generics offer a powerful mechanism for creating reusable components that can work with various data types. This chapter delves into the world of generics, exploring their core concepts, different use cases, and how they help you write flexible and type-safe code in your TypeScript applications.
Generics introduce type parameters, which act as placeholders for specific types within a function, class, or interface definition. These placeholders can be used throughout the definition, allowing the component to work with different data types without code duplication.
function identity(value: T): T {
return value;
}
const numberIdentity = identity(42); // Type of numberIdentity is inferred as number
console.log(numberIdentity); // Output: 42
const stringIdentity = identity("hello"); // Type of stringIdentity is inferred as string
console.log(stringIdentity); // Output: "hello"
const booleanIdentity = identity(true); // Type of booleanIdentity is explicitly set as boolean
console.log(booleanIdentity); // Output: true
const arrayIdentity = identity([1, 2, 3]); // Type of arrayIdentity is explicitly set as number[]
console.log(arrayIdentity); // Output: [1, 2, 3]
Angle brackets (< >
) are used to define the type parameter(s) within a generic definition.
function compare(value1: T, value2: T): boolean {
return value1 === value2;
}
console.log(compare(10, 20)); // Output: false
console.log(compare("hello", "hello")); // Output: true
You can constrain the type parameter to extend a specific interface or class, ensuring the parameter can only be used with types that inherit from that base type.
interface Comparable {
compareTo(other: this): boolean;
}
function compareWithConstraint(value1: T, value2: T): boolean {
return value1.compareTo(value2);
}
// This works as User implements Comparable
class User implements Comparable {
name: string;
age: number;
compareTo(other: User): boolean {
return this.name === other.name && this.age === other.age;
}
}
const user1 = new User();
user1.name = "Alice";
user1.age = 30;
const user2 = new User();
user2.name = "Alice";
user2.age = 30;
console.log(compareWithConstraint(user1, user2)); // Output: true (assuming compareTo logic works)
You can define multiple constraints separated by commas for a type parameter.
type StringOrNumber = string | number;
function addOrConcat(value1: T, value2: T): T {
if (typeof value1 === "string" && typeof value2 === "string") {
return value1 + value2;
} else if (typeof value1 === "number" && typeof value2 === "number") {
return value1 + value2;
} else {
// Handle potential type mismatch if constraints are not met
throw new Error("Incompatible types for addition or concatenation");
}
}
console.log(addOrConcat("hello", " world")); // Output: "hello world"
console.log(addOrConcat(10, 20)); // Output: 30
You can create generic classes with type parameters to represent data structures that can hold various types of data.
class ArrayContainer {
private items: T[] = [];
addItem(item: T): void {
this.items.push(item);
}
getItem(index: number): T {
return this.items[index];
}
}
const numberContainer = new ArrayContainer();
numberContainer.addItem(42);
console.log(numberContainer.getItem(0)); // Output: 42
const stringContainer = new ArrayContainer();
stringContainer.addItem("hello");
console.log(stringContainer.getItem(0)); // Output: "hello"
Similar to classes, you can define generic interfaces to specify the structure of objects that can have different property types.
interface KeyValuePair {
key: K;
value: V;
}
const nameAgePair: KeyValuePair = { key: "Alice", value: 30 };
console.log(nameAgePair); // Output: { key: "Alice", value: 30 }
const productCodePair: KeyValuePair = { key: "SKU123", value: "T-Shirt" };
console.log(productCodePair); // Output: { key: "SKU123", value: "T-Shirt" }
Functions can have multiple type parameters for even greater flexibility.
function swap(value1: T, value2: U): [U, T] {
return [value2, value1];
}
const swappedNumbers = swap(10, 20);
console.log(swappedNumbers); // Output: [20, 10] (type of each element inferred)
const swappedStringNumber = swap("hello", 42);
console.log(swappedStringNumber); // Output: [42, "hello"] (type of each element inferred)
You can use interfaces to define constraints for type parameters, promoting code reusability and type safety.
interface Lengthy {
length: number;
}
function getLength(value: T): number {
return value.length;
}
const textLength = getLength("hello world"); // Works as string has length property
console.log(textLength); // Output: 11
// This would cause a compile-time error as User does not have a length property
class User {
name: string;
age: number;
}
// const userLength = getLength(new User()); // Error: User does not implement Lengthy
Use generics when you need to create functions, classes, or interfaces that can operate on various data types without code duplication.
Generics enhance type safety by explicitly defining the type relationships within your code. This improves code readability and maintainability.
Generics are a powerful tool in your TypeScript arsenal. By understanding their core concepts, different use cases, and best practices, you can write flexible, type-safe, and reusable code that adapts to various data types. Generics can significantly improve the maintainability and expressiveness of your TypeScript projects. Happy coding !❤️