Master TypeScript Generics with this beginner-friendly guide, featuring real-world examples and practical tips to improve your code’s flexibility.
TypeScript has rapidly become the go-to language for frontend developers, and with good reason. Its static type system adds a layer of safety and clarity to JavaScript, preventing common errors and improving code maintainability. One of the most powerful features of TypeScript is Generics. In this article, we’ll dive into TypeScript Generics, starting from the basics and building up to more advanced use cases, with real-world examples that will help you write cleaner and more reusable code.
By the end of this guide, you’ll not only understand the concept of generics but also feel comfortable using them in your day-to-day TypeScript development.
In TypeScript, generics allow you to write functions, classes, and interfaces that can work with any data type while still maintaining full type safety. They allow you to create reusable code components that are flexible yet type-safe.
For example, instead of writing multiple versions of a function that handles different types of data, you can write a single generic function that works for all types. This can drastically reduce code duplication and improve readability.
Here’s a basic example to illustrate the concept:
function identity<T>(value: T): T {
return value;
}
const numberIdentity = identity(1); // number
const stringIdentity = identity("hello"); // string
In this function, T
is a generic type parameter that can represent any type. The function identity
works with any type passed to it, but it ensures that the input type is consistent with the return type.
Generics provide three main benefits:
Type Safety: They help avoid errors by ensuring that the types are consistent throughout your code. For example, a generic function won’t allow you to pass a string when it expects a number, and vice versa.
Reusability: By using generics, you can create functions, classes, and interfaces that work with any type, allowing for more flexible code without sacrificing type safety.
Maintainability: As you scale up your project, generics make it easier to refactor code since you don’t need to maintain multiple versions of functions or interfaces for different types.
Let’s look at how to define a basic generic function. The syntax involves using angle brackets <T>
to define the generic type parameter:
function log<T>(value: T): void {
console.log(value);
}
In this example, T
represents the type of value
. When calling log
, TypeScript can infer the type from the argument you pass to the function:
log("Hello, world!"); // TypeScript infers T as string
log(123); // TypeScript infers T as number
You can also define generics that work with multiple parameters. Here’s an example of a function that accepts two different types:
function combine<T, U>(a: T, b: U): string {
return `${a} and ${b}`;
}
console.log(combine(5, "apples")); // "5 and apples"
In this example, T
represents the type of a
, and U
represents the type of b
. This makes the function flexible, while ensuring that the types of a
and b
are correctly enforced.
Generics are not just limited to functions. They can also be used in interfaces and classes. Let’s look at an example of a generic class that works with any data type:
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const numberBox = new Box(123); // Box<number>
const stringBox = new Box("hello"); // Box<string>
In this example, the Box
class is generic. It accepts a value of type T
, and the getValue
method returns that value, maintaining type safety.
Sometimes, you may want to restrict the types that can be used with a generic. This can be done using constraints. Let’s say you want a function that only works with types that have a length
property (e.g., strings or arrays). You can add a constraint like this:
function logLength<T extends { length: number }>(value: T): void {
console.log(value.length);
}
logLength("Hello, world!"); // Works because string has a length property
logLength([1, 2, 3]); // Works because array has a length property
Here, T extends { length: number }
ensures that the type passed to the function has a length
property, like strings and arrays.
TypeScript comes with several built-in generic utility types that can help you build more flexible and type-safe applications. For example:
T
optional.interface Person {
name: string;
age: number;
}
const person: Partial<Person> = {}; // name and age are optional
T
read-only.const person: Readonly<Person> = { name: "John", age: 30 };
// person.name = "Jane"; // Error: cannot assign to 'name' because it is a read-only property.
K
and values of type T
.type Page = "home" | "about" | "contact";
const pageTitles: Record<Page, string> = {
home: "Welcome",
about: "About Us",
contact: "Contact Us"
};
These utility types can simplify your code, reducing boilerplate while enhancing its type safety.
Generics are useful when working with collections like arrays. Let’s consider a real-world example where you need to filter out elements from an array that don’t match a given type:
function filterArray<T>(arr: T[], predicate: (value: T) => boolean): T[] {
return arr.filter(predicate);
}
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filterArray(numbers, (num) => num % 2 === 0); // [2, 4]
In this example, the filterArray
function works with any type of array (T[]
). The function takes a predicate
function that filters the array based on a condition.
In React development, TypeScript Generics can help you create reusable components that work with any type of props. For instance, consider a modal component:
type ModalProps<T> = {
data: T;
onClose: () => void;
};
function Modal<T>({ data, onClose }: ModalProps<T>) {
return (
<div>
<h1>{data}</h1>
<button onClick={onClose}>Close</button>
</div>
);
}
<Modal data="Hello, World!" onClose={() => {}} />; // Works with string data
<Modal data={123} onClose={() => {}} />; // Works with number data
In this example, the Modal
component is flexible, as it can accept any data type as its data
prop, making it reusable for different use cases.
While generics are a powerful tool, overusing them can make your code harder to read and maintain. Use generics when it truly adds value, like when writing reusable components or functions that operate on multiple types.
TypeScript often infers types for generics, but there are times when you’ll want to explicitly specify the type. Be mindful of when inference is sufficient, and when you should specify the type for clarity and correctness.
When working with generics, ensure you handle edge cases properly, especially when you apply constraints. Always check for potential issues when narrowing types and make sure to test your code thoroughly.
By now, you should have a solid understanding of TypeScript Generics and their practical applications. Let’s recap the key takeaways:
Partial
, Readonly
, and Record
for even more flexibility.Generics are a powerful tool in TypeScript, and mastering them will greatly improve your ability to write clean, efficient, and maintainable code.