I am building a website using Next.js 15.3.3, React 19, and Sanity.io for content. Everything works perfectly in my local development environment (next dev). However, when I deploy to Vercel, the build fails during the "Checking validity of types" step with a TypeScript error on my dynamic pages (like app/category/[slug]/page.tsx).
Error:
Failed to compile.
app/category/[slug]/page.tsx
Type error: Type 'CategoryPageProps' does not satisfy the constraint 'PageProps'.
Types of property 'params' are incompatible.
Type '{ slug: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally, [Symbol.toStringTag]
Here is the code for the failing page, app/category/[slug]/page.tsx:
// app/category/[slug]/page.tsx
import { client as sanityClient } from "@/lib/sanity";
import { createClient as createServerSupabaseClient } from "@/lib/supabase/server";
import { notFound } from "next/navigation";
import ProductCard from "@/components/ProductCard";
import ProductFilters from "@/components/ProductFilters";
interface CategoryPageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
async function getData(slug: string, searchParams: CategoryPageProps['searchParams']) {
const sort = typeof searchParams.sort === 'string' ? searchParams.sort : 'latest';
const minPrice = typeof searchParams.minPrice === 'string' ? parseFloat(searchParams.minPrice) : undefined;
const maxPrice = typeof searchParams.maxPrice === 'string' ? parseFloat(searchParams.maxPrice) : undefined;
const inStock = searchParams.inStock === 'true';
// 1. Build filter and order clauses for GROQ
let orderClause = '';
if (sort === 'price-asc') orderClause = '| order(price asc)';
if (sort === 'price-desc') orderClause = '| order(price desc)';
if (sort === 'latest') orderClause = '| order(_createdAt desc)';
const filterClauses: string[] = [];
if (minPrice) filterClauses.push(`price >= ${minPrice}`);
if (maxPrice) filterClauses.push(`price <= ${maxPrice}`);
if (inStock) filterClauses.push(`stock > 0`);
const categoryQuery = `*[_type == "category" && slug.current == $slug][0]{ title, description }`;
const productsQuery = `*[_type == "product" && references(*[_type=="category" && slug.current == $slug]._id) ${filterClauses.length > 0 ? `&& ${filterClauses.join(' && ')}` : ''}] ${orderClause} {
_id, name, slug, price, compareAtPrice, images
}`;
const category = await sanityClient.fetch(categoryQuery, { slug });
const products = await sanityClient.fetch(productsQuery, { slug });
return { category, products };
}
export default async function CategoryPage({
params,
searchParams,
}: {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}) {
const { category, products } = await getData(params.slug, searchParams);
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!category) {
return notFound();
}
return (
<>
<header className="bg-gradient-to-b from-accent/80 to-background">
<div className="max-w-4xl mx-auto text-center pt-20 pb-12 px-4">
<h1 className="text-4xl font-serif tracking-tight text-foreground sm:text-6xl">
{category.title}
</h1>
{category.description && (
<p className="mt-6 max-w-2xl mx-auto text-lg text-foreground/80">
{category.description}
</p>
)}
</div>
</header>
{/* --- MAIN CONTENT AREA --- */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* The filter bar sits on top of the product grid */}
<ProductFilters />
<div className="mt-8">
{products.length > 0 ? (
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 gap-4 md:gap-6">
{products.map((product: any) => (
<ProductCard key={product._id} product={product} userId={user?.id} />
))}
</div>
) : (
<div className="text-center py-16 border-2 border-dashed rounded-lg">
<p className="text-lg text-muted-foreground">No products found matching your filters.</p>
</div>
)}
</div>
</main>
</>
);
}
What I've already Tried:
- The code works perfectly on localhost.
- Adding a // @ts-expect-error comment above the function causes an "Unused directive" error locally but still fails on Vercel.
- Changing the prop types to any ({ params }: any) still results in a build failure.
- Adding the --no-lint flag to my build script confirms this is a TypeScript compiler error, not an ESLint rule violation.
- Await the params to get the slug
What is the correct, official way to type the params prop for a dynamic Server Component in Next.js 15 to avoid this build error on Vercel?