TypeScript, a statically typed superset of JavaScript, has gained immense popularity among developers for its ability to catch errors at compile-time and improve code quality. One of TypeScript’s most powerful features is generics, which allow you to write flexible, reusable code that works with multiple data types while maintaining type safety. In this article, we’ll dive deep into TypeScript generics, exploring how they can help you create more robust and adaptable components and functions.
What are Generics?
When I first encountered generics, I thought of them as a way to create reusable code components that can work with different data types. Instead of specifying a particular type, we use a type parameter that acts as a placeholder. This allows us to write functions, classes, and interfaces that can operate on various data types while still providing type checking and intellisense support.
Why should we use Generics?
In my experience, there are several compelling reasons to use generics:
- Type Safety: Generics provide compile-time type checking, helping us catch errors early.
- Code Reusability: We can write once and use with multiple types.
- Flexibility: We can create components that work with various data structures.
- Better Intellisense: Our IDEs can provide better code completion and hints.
Basic Syntax
Let’s start with a simple example to illustrate the syntax of generics:
function identity<T>(arg: T): T { return arg;
} let output1 = identity<string>("Hello, Generics!");
let output2 = identity(42); console.log(output1); // "Hello, Generics!"
console.log(output2); // 42
In this example, we use <T>
as a type parameter. When we call the function, we can either explicitly specify the type (as in output1
) or let TypeScript infer it based on the argument (as in output2
).
Practical Examples
Generic Functions
Let’s look at a more practical example. Here’s a function I often use to reverse an array of any type:
function reverseArray<T>(array: T[]): T[] { return array.reverse();
} const numbers = [1, 2, 3, 4, 5];
const reversedNumbers = reverseArray(numbers);
console.log(reversedNumbers); // [5, 4, 3, 2, 1] const fruits = ["apple", "banana", "cherry"];
const reversedFruits = reverseArray(fruits);
console.log(reversedFruits); // ["cherry", "banana", "apple"]
This reverseArray
function works with arrays of any type, maintaining type safety throughout.
Generic Interfaces
I’ve found that generics are not limited to functions. We can also use them with interfaces to create flexible data structures:
interface KeyValuePair<TKey, TValue> { key: TKey; value: TValue;
} let pair1: KeyValuePair<string, number> = { key: "age", value: 30 };
let pair2: KeyValuePair<number, boolean> = { key: 1, value: true };
This KeyValuePair
interface can be used with different types for both the key and the value.
Generic Classes
In my projects, I’ve found generics particularly useful when creating reusable class components. Here’s an example of a generic Stack
class I often use:
class Stack<T> { private items: T[] = []; push(item: T): void { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } peek(): T | undefined { return this.items[this.items.length - 1]; } isEmpty(): boolean { return this.items.length === 0; }
} const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3); console.log(numberStack.pop()); // 3
console.log(numberStack.peek()); // 2 const stringStack = new Stack<string>();
stringStack.push("Hello");
stringStack.push("World"); console.log(stringStack.pop()); // "World"
console.log(stringStack.isEmpty()); // false
This Stack
class can be used with any data type, providing type-safe operations for pushing, popping, and peeking at elements.
Advanced Generic Techniques
Constraints
Sometimes we want to restrict the types that can be used with our generic components. We can do this using constraints:
interface Lengthwise { length: number;
} function logLength<T extends Lengthwise>(arg: T): void { console.log(arg.length);
} logLength("Hello"); // 5
logLength([1, 2, 3]); // 3
logLength({ length: 10 }); // 10
// logLength(3); // Error: Argument of type 'number' is not assignable to parameter of type 'Lengthwise'
In this example, T extends Lengthwise
ensures that the argument passed to logLength
has a length
property.
Generic Type Aliases
I’ve found that type aliases can also leverage generics:
type Nullable<T> = T | null;
type Pair<T> = [T, T]; let nullableNumber: Nullable<number> = 5;
nullableNumber = null; let coordinates: Pair<number> = [10, 20];
Conclusion
Generics are a fantastic feature of TypeScript that help us write code that is both flexible and type-safe. By leveraging generics, we can build reusable components and functions that work with various types while ensuring type consistency. We hope this guide gives us a solid understanding of how to use generics effectively in our TypeScript projects.
Let’s experiment with generics in our own code and see how they can make our development process smoother and more efficient!
Drop me an email here: [email protected]
Follow me on: 🙋🏻♂️
Explore my portfolio
Welcome to my professional portfolio website—a curated glimpse into my professional world. Here, you’ll find:
🌟 A collection of standout projects highlighting my expertise
🚀 Insights into my career trajectory and key achievements
💼 A showcase of my diverse skills and competencies
Whether you’re seeking inspiration, exploring collaboration opportunities, or simply curious about my work, I invite you to peruse my portfolio.
Your visit could be the first step towards a valuable professional connection. 🤝 Thank you for your interest—I look forward to the possibilities our interaction might bring. 😊
Discover more from Coursity
Subscribe to get the latest posts sent to your email.