reactjstypescriptnext.jseslintvercel

Vercel build fails with TypeScript error 'params' is a Promise on Next.js 15 Server Component


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:

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?


Solution

  • As per the NextJs documentation both the slug and the searchParams are Promises which have to be awaited.
    That's also what the error is pointing out:

      Type '{ slug: string; }' is missing the following properties from type 'Promise<any>': then, catch, finally, [Symbol.toStringTag]
    

    For the solution you need to change the definition of your CategoryPageProps to this:

    interface CategoryPageProps {
      params: Promise<{ slug: string }>;
      searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
    }
    

    And then in your CategoryPage function export you'll have to await these promises:

    export default async function CategoryPage({
      params,
      searchParams,
    }: CategoryPageProps) {
      const { slug } = await params;
      const resolvedSearchParams = await searchParams;
      
      const { category, products } = await getData(slug, resolvedSearchParams);
      //...
    

    You will also want to change the signature of the getData function to this:

    async function getData(
      slug: Awaited<CategoryPageProps['params']>['slug'], 
      searchParams: Awaited<CategoryPageProps['searchParams']>
    ) {
        //...
    

    If you want to learn more about types I'd suggest the typescript documentation and the typescript playground