Generics in Next.js might not be the first thing that comes to mind when you think about building React applications with server-side rendering, but they play a crucial role, especially when you’re working with TypeScript. Using generics effectively can make your code more flexible, maintainable, and type-safe, which is a huge win in any production app.
Let me break down how generics fit into Next.js development, why you’d want to use them, and some practical tips based on real-world experience.
Generics are a TypeScript feature that allows you to write reusable, type-safe components, functions, or hooks that work with a variety of data types without losing type information. Instead of hardcoding types, generics let you define placeholders that get replaced with actual types when you use them.
In Next.js, you’re often dealing with data fetching, API responses, and component props that can vary a lot depending on the page or feature. Generics help you keep your types consistent and avoid repetitive type declarations.
getStaticProps, getServerSideProps, or custom API calls, generics help type the shape of the data you expect.One of the most common places to use generics in Next.js is in the data fetching methods. For example, getStaticProps and getServerSideProps expect you to return a props object, but the shape of that object varies depending on the page.
Here’s a typical pattern:
import { GetStaticProps, NextPage } from 'next';
interface Post {
id: number;
title: string;
content: string;
}
interface Props {
posts: Post[];
}
export const getStaticProps: GetStaticProps<Props> = async () => {
const res = await fetch('https://api.example.com/posts');
const posts: Post[] = await res.json();
return {
props: {
posts,
},
};
};
const BlogPage: NextPage<Props> = ({ posts }) => {
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
))}
</div>
);
};
export default BlogPage;
Notice how GetStaticProps<Props> is used to type the return value of the data fetching function. This ensures that the props passed to the page component match the expected shape, catching errors at compile time if you return something unexpected.
Another practical use case is in custom React hooks that fetch data. Instead of creating a new hook for every data type, you can write a generic hook that accepts a type parameter.
import { useState, useEffect } from 'react';
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then(res => res.json())
.then((json: T) => {
setData(json);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
// Usage example:
interface User {
id: number;
name: string;
}
const UserProfile = () => {
const { data: user, loading, error } = useFetch<User>('/api/user/123');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>Hello, {user?.name}</div>;
};
This pattern keeps your hooks reusable and strongly typed. You get the benefits of type inference without duplicating code for each data shape.
T, use meaningful names like PostType or UserData when it improves clarity.Partial, Pick, or Record can work well with generics to model partial or dynamic data.tsd or write small test files to verify your generic types behave as expected.any inside generics: This defeats the purpose of generics and leads to loss of type safety.Generics themselves don’t impact runtime performance because they are erased during compilation. However, they significantly improve developer productivity and code quality, which indirectly affects scalability and maintainability.
In large Next.js projects, having well-typed generic components and hooks means fewer bugs, easier refactoring, and better onboarding for new developers. This pays off when your app grows and you need to iterate quickly.
One thing to watch out for is overusing generics in deeply nested components or hooks, which can sometimes lead to complicated type errors that slow down your development workflow. In those cases, balancing type safety with simplicity is key.
While generics don’t directly affect security, strong typing helps prevent common bugs that can lead to vulnerabilities. For example, when you type your API responses and input data correctly, you reduce the risk of injection attacks or unexpected data shapes causing issues.
Always validate and sanitize data on the server side, regardless of TypeScript types, because types are erased at runtime. Generics help catch mistakes early but don’t replace runtime validation.
getStaticProps or custom hooks where generics shine.any.| Approach | Pros | Cons | Use Case |
|---|---|---|---|
| Generics | Flexible, reusable, type-safe, improves DX | Can be complex, harder to read if overused | Reusable components, hooks, typed data fetching |
| Explicit Types (non-generic) | Simple, easy to understand | Less reusable, more duplication | Small components or pages with fixed data shapes |
| Any / Unknown Types | Quick to implement, flexible | No type safety, prone to runtime errors | Prototyping or legacy code |
Imagine you’re building a multi-tenant SaaS app with Next.js where each tenant has different user profile shapes. Instead of writing separate components or hooks for each tenant, you can create a generic UserProfile component:
interface UserBase {
id: string;
email: string;
}
interface TenantAUser extends UserBase {
subscriptionLevel: string;
}
interface TenantBUser extends UserBase {
permissions: string[];
}
function UserProfile<T extends UserBase>({ user }: { user: T }) {
return (
<div>
<p>User ID: {user.id}</p>
<p>Email: {user.email}</p>
{/* Render tenant-specific info */}
</div>
);
}
// Usage:
const tenantAUser: TenantAUser = { id: '1', email: 'a@example.com', subscriptionLevel: 'pro' };
const tenantBUser: TenantBUser = { id: '2', email: 'b@example.com', permissions: ['read', 'write'] };
<UserProfile user={tenantAUser} />
<UserProfile user={tenantBUser} />
This approach keeps your code DRY and type-safe, making it easier to maintain as your app scales.