Generics in TypeScript: Write Flexible and Type-Safe Code
Generics are a cornerstone of TypeScript, enabling developers to write flexible and reusable code. If you’ve ever found yourself writing similar functions or components that differ only in the types they handle, generics are your new best friend. In this post, we’ll explore the magic of generics through simple, engaging examples that will leave you wondering how you ever coded without them!
What Are Generics?
Generics allow you to create components or functions that work with any data type while maintaining type safety. Think of them as placeholders for types that are specified when the function or component is used. This means you can write code that is both flexible and type-safe.
The Problem Without Generics
Imagine you have a function that returns the first element of an array:
function getFirstElementString(arr: string[]): string {
return arr[0];
}
function getFirstElementNumber(arr: number[]): number {
return arr[0];
}
You’d need to write separate functions for each type—not ideal for maintainability or scalability.
The Solution With Generics
Generics solve this by allowing you to define a type parameter:
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
Here, T
is a type parameter that gets replaced with the actual type when the function is called.
Getting Started with Generics
Basic Example: A Generic Identity Function
Let’s start with a simple example: a function that returns its input unchanged.
function identity<T>(arg: T): T {
return arg;
}
let outputString = identity<string>("hello"); // Type is string
let outputNumber = identity<number>(42); // Type is number
In this example, T
acts as a placeholder for the type of the argument and return value.
Using Type Inference
TypeScript can often infer the type parameter, so you don’t always need to specify it explicitly:
let output = identity("hello"); // Type is inferred as string
Working with Arrays and Generics
Generics shine when working with collections like arrays.
Example: Generic Array Function
Let’s create a function that logs the first element of an array:
function logFirstElement<T>(arr: T[]): void {
console.log(arr[0]);
}
logFirstElement([1, 2, 3]); // Logs 1
logFirstElement(["a", "b", "c"]); // Logs "a"
Generic Array Helper Functions
You can also create reusable helper functions for arrays:
function getLastElement<T>(arr: T[]): T {
return arr[arr.length - 1];
}
let lastString = getLastElement(["a", "b", "c"]); // "c"
let lastNumber = getLastElement([1, 2, 3]); // 3
Generics in Interfaces
Generics aren’t limited to functions; you can use them in interfaces too.
Example: Generic Box Interface
Imagine you want an interface that represents a box containing a value of any type:
interface Box<T> {
value: T;
}
let stringBox: Box<string> = { value: "hello" };
let numberBox: Box<number> = { value: 42 };
This approach ensures that the value
property maintains its type integrity.
Generic Classes
Generics are incredibly powerful in classes, allowing you to create reusable class components.
Example: Generic Stack Class
Let’s create a stack class that can handle any type:
class Stack<T> {
private elements: T[] = [];
push(element: T): void {
this.elements.push(element);
}
pop(): T | undefined {
return this.elements.pop();
}
peek(): T | undefined {
return this.elements[this.elements.length - 1];
}
}
let stringStack = new Stack<string>();
stringStack.push("hello");
console.log(stringStack.peek()); // "hello"
let numberStack = new Stack<number>();
numberStack.push(42);
console.log(numberStack.peek()); // 42
Constraints: Making Generics More Specific
Sometimes, you want to constrain the types that can be used with a generic.
Example: Constrained Generic Function
Let’s create a function that only works with types that have a length
property:
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // Logs 5
logLength([1, 2, 3]); // Logs 3
// logLength(42); // Error: Number doesn't have a length property
Here, T extends { length: number }
ensures that only types with a length
property can be used.
Conclusion
Generics are a powerful feature in TypeScript that enable you to write flexible, reusable, and type-safe code. Whether you’re working with functions, interfaces, or classes, generics can help you reduce duplication and improve maintainability.
Happy coding!