typescriptnext.jsnext.js13react-typescript

Type "AppRouterInstance" is not assignable to type "nextRouter"


In my Next.js project, I have a registration form as shown below:

"use client";

import * as React from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import * as z from "zod";

import { cn } from "@/lib/util";
import { userAuthSchema } from "@/lib/validations/auth";
import { buttonVariants } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Icons } from "@/components/icons";
import { useRouter } from "next/navigation";
import { registerUser } from "@/lib/register-user";

export type FormData = z.infer<typeof userAuthSchema>;

export function RegisterForm() {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormData>({ resolver: zodResolver(userAuthSchema) });

  const [isLoading, setIsLoading] = React.useState<boolean>(false);
  const [error, setError] = React.useState<string | null>(null);

  const router = useRouter();

  async function onSubmit(submittedData: FormData) {
    await registerUser({submittedData, setError, setIsLoading, reset, router})
  }


  return (
    <div>
      {error && <p className="mb-5 text-sm text-red-500 animate-in fade-in-0 slide-in-from-left-1">{error}</p>}
      <form onSubmit={handleSubmit(onSubmit)}>
        <div className="grid gap-6">
          <div className="grid gap-1">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              type="email"
              placeholder="name@example.com"
              autoCapitalize="none"
              autoComplete="email"
              autoCorrect="off"
              disabled={isLoading}
              {...register("email")}
            />
            {errors.email && (
              <p className="px-1 text-xs text-red-600 animate-in fade-in-0 slide-in-from-left-1">
                {errors.email.message}
              </p>
            )}
          </div>
          <div className="grid gap-1">
            <Label htmlFor="password">Password</Label>
            <Input
              id="password"
              type="password"
              autoCapitalize="none"
              autoComplete="off"
              autoCorrect="off"
              disabled={isLoading}
              {...register("password")}
            />
            {errors.password && (
              <p className="px-1 text-xs text-red-600 animate-in fade-in-0 slide-in-from-left-1">
                {errors.password.message}
              </p>
            )}
          </div>
          <button className={cn(buttonVariants())} disabled={isLoading}>
            {isLoading && (
              <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />
            )}
            Register
          </button>
        </div>
      </form>
    </div>
  );
}

Notice that I am passing a router argument to the registerUser() function:

  async function onSubmit(submittedData: FormData) {
    await registerUser({submittedData, setError, setIsLoading, reset, router})
  }

And here is the registerUser function:

import "client-only";
import type { FormData } from "@/components/auth/register-form";
import { NextRouter } from "next/router";

const RESPONSE_MESSAGES = {
  USER_DOCUMENT_CREATED: "New user document created",
  USER_DOCUMENT_UPDATED: "Existing user document updated",
  USER_ALREADY_REGISTERED: "User already registered",
  EMAIL_VERIFICATION_LINK_SENT: "Email verification link sent",
  EMAIL_VERIFICATION_LINK_ALREADY_SENT:
    "Email verification link already sent. Please check your inbox and verify your email.",
  REGISTRATION_FAILED: "Registration failed. Please try again.",
};

interface RegisterUserProps {
  submittedData: FormData;
  setError: React.Dispatch<React.SetStateAction<string | null>>;
  reset: () => void;
  setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
  router: NextRouter;
}

export async function registerUser({
  submittedData,
  setError,
  reset,
  setIsLoading,
  router,
}: RegisterUserProps) {
  setIsLoading(true);
  setError(null);

  try {
    // Attempt to register the user
    const registerResponse = await fetch("/api/register", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(submittedData),
    });

    // Registration failed
    if (!registerResponse.ok) {
      throw new Error(RESPONSE_MESSAGES.REGISTRATION_FAILED);
    }

    const registerResponseData = await registerResponse.json();

    // User already registered
    if (
      registerResponseData.message === RESPONSE_MESSAGES.USER_ALREADY_REGISTERED
    ) {
      setError(RESPONSE_MESSAGES.USER_ALREADY_REGISTERED);
      reset();
      return;
    }

    // Email verification link was already sent
    if (
      registerResponseData.message ===
      RESPONSE_MESSAGES.EMAIL_VERIFICATION_LINK_ALREADY_SENT
    ) {
      setError(RESPONSE_MESSAGES.EMAIL_VERIFICATION_LINK_ALREADY_SENT);
      reset();
      return;
    }

    // New user. Send email verification link
    if (
      registerResponseData.message ===
        RESPONSE_MESSAGES.USER_DOCUMENT_CREATED ||
      registerResponseData.message === RESPONSE_MESSAGES.USER_DOCUMENT_UPDATED
    ) {
      const { email, emailVerificationToken } = registerResponseData;

      const emailResponse = await fetch(
        "/api/send-registration-email-verification-link",
        {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify({ email, emailVerificationToken }),
        },
      );

      // Check if email was successfully sent
      if (!emailResponse.ok) {
        throw new Error(RESPONSE_MESSAGES.REGISTRATION_FAILED);
      }

      const emailResponseData = await emailResponse.json();

      if (
        emailResponseData.message ===
        RESPONSE_MESSAGES.EMAIL_VERIFICATION_LINK_SENT
      ) {
        reset();
        router.replace("/register/verify-email");
      }
    }
  } catch (error) {
    setError((error as Error).message);
  } finally {
    setIsLoading(false);
  }
}

As you can see, I have type annotated router with NextRouter:

interface RegisterUserProps {
  submittedData: FormData;
  setError: React.Dispatch<React.SetStateAction<string | null>>;
  reset: () => void;
  setIsLoading: React.Dispatch<React.SetStateAction<boolean>>;
  router: NextRouter;
}

However, when I do this, I am getting a red squiggly line under router in the RegisterForm component.

enter image description here

The error says the following:

Type 'AppRouterInstance' is not assignable to type 'NextRouter'.
  Type 'AppRouterInstance' is missing the following properties from type 'BaseRouter': route, pathname, query, asPath, and 2 more.ts(2322)
register-user.ts(20, 3): The expected type comes from property 'router' which is declared here on type 'RegisterUserProps'
(property) RegisterUserProps.router: NextRouter

Can someone explain me what's going on here? And how can I fix this TypeScript error?


Solution

  • Next.js has two useRouters The old one can be imported from 'next/router' and the new one in v13+ is imported from 'next/navigation`

    The old useRouter type is NextRouter and the new one is AppRouterInstance

    You used the new in your component and typed the old in your function.

    One tip I used to do for similar situations is to noy import the type but to let Typescript infer the type of the function by: UseRouter: typeof useRouter and to get the the return type use the ReturnType helper type router = ReturnType<typeof useRouter>

    Update

    Because there is no real need for this type the Next.js team doesn't export it directly. But you can still import the internal type import { AppRouterInstance } from "next/dist/shared/lib/app-router-context"