TypeScript is a powerhouse when it comes to building scalable, maintainable applications. One of its standout features is the ability to define generic types, a tool that provides flexibility and reusability across various scenarios. In this blog, we’ll explore the concept of generics using some practical examples that illustrate their power and utility.
What Are Generics?
Generics allow you to define components, functions, or types that can work with a variety of data types while maintaining type safety. Instead of hardcoding a specific type, generics let you use placeholders (like T
, Data
, or Props
) to represent types, which are then replaced with actual types at runtime.
In essence, generics provide type parameters that make your code adaptable and reusable.
Example 1: Creating a Paginated Type
Pagination is a common feature in applications. A generic type can help define a consistent shape for paginated responses, regardless of the data type being paginated. Here’s how you can use generics for this purpose:
typescripttype Paginated<Data> = {
data: Data[]
page: number
take: number
total: number
}
Breaking It Down:
-
Data
is the generic type placeholder that represents the type of the paginated data. -
data: Data[]
ensures that thedata
property is always an array of the specified type. -
Other properties like
page
,take
, andtotal
are standard fields in a paginated response.
Example Usage:
Imagine you’re paginating a list of users and posts. With the Paginated
type, you can handle both scenarios without duplicating code:
typescripttype User = {
id: number
name: string
}
type Post = {
id: number
title: string
content: string
}
// Paginated users
const paginatedUsers: Paginated<User> = {
data: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
],
page: 1,
take: 10,
total: 50
}
// Paginated posts
const paginatedPosts: Paginated<Post> = {
data: [
{ id: 1, title: "Intro to TypeScript", content: "TypeScript is awesome!" },
{ id: 2, title: "Generics 101", content: "Learn how to use generics." }
],
page: 2,
take: 5,
total: 20
}
By using generics, the Paginated
type adapts seamlessly to different data types, maintaining strict type safety.
Example 2: A Parent Component Type for React
In React, many components accept children
as props. You can define a reusable type that ensures type safety for both the component’s props and its children:
typescripttype ParentComponent<Props = object> = Props & {
children: React.ReactNode
}
Breaking It Down:
-
Props = object
defines a generic type with a default value (object
). If no specific type is provided,Props
defaults toobject
. -
The
&
operator mergesProps
with{ children: React.ReactNode }
, ensuring that any component using this type can handle additional props and children.
Example Usage:
Here’s how you can use ParentComponent
in a real-world scenario:
typescripttype CardProps = {
title: string
}
const Card: React.FC<ParentComponent<CardProps>> = ({
title, children
}) =>
<div className="card">
<h2>{title}</h2>
<div>{children}</div>
</div>
// Using the Card component
<Card title="Welcome">
<p>This is a reusable card component with generics.</p>
</Card>
This type ensures that:
-
The
title
prop is required for theCard
component. -
The
children
prop is always valid and type-safe.
Example 3: Next.js Page Component with Context
Neneric types can make page components more robust by defining the structure of context and search parameters. Here’s a generic type to manage them:
typescripttype PageComponent<Context = object> = {
params: Promise<Context>
searchParams: Promise<Record<string, string | undefined>>
}
Promise
s are required after version 15 in Next.js
Breaking It Down:
-
Context = object
allows developers to specify the shape of the context object for the page. -
params: Promise<Context>
ensures thatparams
will always resolve to a value of typeContext
. -
searchParams: Promise<Record<string, string | undefined>>
handles query parameters, mapping keys to strings orundefined
.
Example Usage:
Suppose you’re building a page that displays a blog post list based on its category slug. You can define a page component type like this:
typescripttype CategoryContext = {
category: string
}
// For the URL /blogs/software?sort=desc&filter=popular
const CategoryPage: React.FC<PageComponent<CategoryContext>> = async ({
params, searchParams
}) => {
const resolvedParams = await params
const resolvedSearchParams = await searchParams
console.log(resolvedParams) // { category: "typescript-generics" }
console.log(resolvedSearchParams) // { sort: "desc", filter: "popular" }
return <div>
<h1>Slug: {resolvedParams.slug}</h1>
<p>Sort: {resolvedSearchParams.sort}</p>
</div>
}
This ensures type safety when accessing the params
and searchParams
, reducing runtime errors and improving code readability.
Why Use Generics?
- Reusability: Generic types can be applied to various data structures or components, reducing redundancy.
- Type Safety: They provide stricter type checking, catching potential errors during development.
- Flexibility: Generics allow you to handle diverse use cases without sacrificing maintainability.
Conclusion
Generics are a fundamental part of TypeScript that make your code more reusable, flexible, and maintainable. Whether you’re handling paginated data, building reusable React components, or managing complex contexts in Next.js, generics can simplify and strengthen your codebase.
The examples above highlight the versatility of generics in TypeScript. By embracing this feature, you can write cleaner, safer, and more adaptable code that scales with your application’s needs.