ethanniser / next-typesafe-url

Fully typesafe, JSON serializable, and zod validated URL search params, dynamic route params, and routing for NextJS.
https://next-typesafe-url.dev
MIT License
362 stars 16 forks source link

Route with dynamic segment considered as static route. #81

Closed lelabo-m closed 6 months ago

lelabo-m commented 7 months ago

Hi, I have picked up your package recently (due to Theo's video about Tanstack Router).

I have a page with the following URL http://localhost:3000/food/28881

I defined my routeType.ts as follow:

import type { DynamicRoute } from "next-typesafe-url";
import { z } from "zod";

export const Route = {
  routeParams: z.object({
    foodId: z.number(),
  }),
} satisfies DynamicRoute;

export type RouteType = typeof Route;

But when I try to use it in a Link, routeParams is underlined with the error message: Type '{ foodId: number; }' is not assignable to type 'undefined'.

<Link href={$path({route: "/food/[foodId]", routeParams: { foodId: food.id } })} >
{food.name}
</Link>

My codegen file look like this:

import { type RouteType as Route_0 } from "./src/app/food/routeType";
import type { InferRoute, StaticRoute } from "next-typesafe-url";

declare module "@@@next-typesafe-url" {
  interface DynamicRouter {
    "/food": InferRoute<Route_0>;
  }

  interface StaticRouter {
    "/about/fodmap": StaticRoute;
    "/food/[foodId]": StaticRoute;
    "/": StaticRoute;
    "/quiz/willitfodmap": StaticRoute;
  }
}

From what I understood about the package, I think my route should not be in the StaticRouter. It should be a DynamicRoute.

The error might be due to not using searchParams on this route?

My package.json looks like this:

{
  "type": "module",
  "scripts": {
    "build": "next-typesafe-url && next build",
    "db:push": "dotenv drizzle-kit push:mysql",
    "db:studio": "dotenv drizzle-kit studio",
    "db:ciqual:load": "tsx ./scripts/import-ciqual-data.ts",
    "script": "dotenv tsx",
    "dev": "concurrently  \"next-typesafe-url -w\" \"next dev --turbo\"",
    "dev:url": "next-typesafe-url -w",
    "lint": "next lint",
    "start": "next start"
  },
  "dependencies": {
    "@auth/drizzle-adapter": "^0.3.12",
    "@planetscale/database": "^1.13.0",
    "@radix-ui/react-dropdown-menu": "^2.0.6",
    "@radix-ui/react-slot": "^1.0.2",
    "@radix-ui/react-toggle": "^1.0.3",
    "@radix-ui/react-toggle-group": "^1.0.4",
    "@t3-oss/env-nextjs": "^0.7.1",
    "@tanstack/react-query": "^4.36.1",
    "@trpc/client": "^10.45.0",
    "@trpc/next": "^10.45.0",
    "@trpc/react-query": "^10.45.0",
    "@trpc/server": "^10.45.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.0",
    "drizzle-orm": "^0.28.6",
    "geist": "^1.2.0",
    "lucide-react": "^0.294.0",
    "next": "^14.0.4",
    "next-auth": "^4.24.5",
    "next-themes": "^0.2.1",
    "next-typesafe-url": "^4.0.3",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "server-only": "^0.0.1",
    "superjson": "^2.2.1",
    "tailwind-merge": "^2.2.0",
    "tailwindcss-animate": "^1.0.7",
    "zod": "^3.22.4",
    "zustand": "^4.4.7"
  },
  "devDependencies": {
    "@next/eslint-plugin-next": "^14.0.4",
    "@types/eslint": "^8.56.0",
    "@types/node": "^18.19.4",
    "@types/papaparse": "^5.3.14",
    "@types/react": "^18.2.46",
    "@types/react-dom": "^18.2.18",
    "@typescript-eslint/eslint-plugin": "^6.17.0",
    "@typescript-eslint/parser": "^6.17.0",
    "autoprefixer": "^10.4.16",
    "concurrently": "^8.2.2",
    "dotenv": "^16.3.1",
    "dotenv-cli": "^7.3.0",
    "drizzle-kit": "^0.19.13",
    "eslint": "^8.56.0",
    "mysql2": "^3.6.5",
    "papaparse": "^5.4.1",
    "postcss": "^8.4.32",
    "prettier": "^3.1.1",
    "prettier-plugin-tailwindcss": "^0.5.10",
    "tailwindcss": "^3.4.0",
    "tsx": "^4.7.0",
    "typescript": "^5.3.3"
  },
  "ct3aMetadata": {
    "initVersion": "7.24.0"
  },
  "packageManager": "pnpm@7.11.0"
}
lelabo-m commented 7 months ago

Should my codegen look like this instead?

import { type RouteType as Route_0} from "./src/app/food/routeType";
import { type RouteType as Route_1} from "./src/app/food/[foodId]/routeType";
import type { InferRoute, StaticRoute } from "next-typesafe-url";

declare module "@@@next-typesafe-url" {
  interface DynamicRouter {
    "/food": InferRoute<Route_0>;
    "/food/[foodId]": InferRoute<Route_1>;
  }

  interface StaticRouter {
    "/about/fodmap": StaticRoute;
    "/": StaticRoute;
    "/quiz/willitfodmap": StaticRoute;
  }
}
lelabo-m commented 7 months ago

OK, I found why my _next-typesafe-url_.d.ts was not generated as expected. My "/food/[foodId]" routeType file was the only one with a .tsx instead of a .ts (is there a reason why .tsx are not supported?).

However, I am still not able to build the project. The $path error is still there.

Type error: Type '{ foodId: number; }' is not assignable to type 'undefined'.

  26 |                 href={$path({
  27 |                   route: "/food/[foodId]",
> 28 |                   routeParams: { foodId: food.id },
     |                   ^
  29 |                 })}
  30 |               >
  31 |                 {food.name}
 ELIFECYCLE  Command failed with exit code 1.

There is a problem with the PathOptions<T> generation for $path() but I don't understand why yet.

ethanniser commented 7 months ago

The intention with only supporting routeType.ts and not routeType.tsx is that there really shouldnt be any jsx code in that file. Ideally the only thing there is the Route and RouteType declaration and export.

As for the $path error, someone else reported something similar- but it is very difficult to diagnose the problem without a reproduction. Are you able to share the code, or a example app with the same problem?

Thanks for trying out the package and reporting the issue

lelabo-m commented 7 months ago

The intention with only supporting routeType.ts and not routeType.tsx is that there really shouldnt be any jsx code in that file. Ideally the only thing there is the Route and RouteType declaration and export.

I understand I create many .tsx in my pages by inattention, and it is a bad habit. Not supporting .tsx makes sense, I was just curious.

For the $path(), I created a minimal reproducible example there: https://github.com/lelabo-m/error-reproduction-minimal-env/tree/next-typesafe-url/path-parameters-inference

If you open the project, and go to the root page (./src/app/page.tsx), you should be able to see that typescript isn't happy with the parameters given to $path.

image_2024-01-12_174148781

ethanniser commented 7 months ago

thank you I will take a look this weekend

lelabo-m commented 6 months ago

I managed to understand what was going on.

_next-typesafe-url_.d.ts is not treated as an ambient file because of the imports in the start of the file. Therefore, the following is not working:

import { StaticRouter, DynamicRouter } from '@@@next-typesafe-url'; // does not work.

[...]

type Test = "/a_dynamic_route" extends StaticRoutes ? true : false; // will always return true.

I am not an expert on the subject so maybe a specific tsconfig.json or modifying the codegen output path could fix this.

What I learn from this message on StackOverflow, is that import could be moved into the declaration to make the .d.ts file ambient.

Example of conversion that work:

FROM

import { type RouteType as Route_0 } from "./src/app/food/[foodId]/routeType";
import { type RouteType as Route_1 } from "./src/app/food/routeType";
import { type RouteType as Route_2 } from "./src/app/quiz/[quizId]/routeType";
import { type RouteType as Route_3 } from "./src/app/quiz/routeType";
import type { InferRoute, StaticRoute } from "next-typesafe-url";

declare module "@@@next-typesafe-url" {
  interface DynamicRouter {
    "/food/[foodId]": InferRoute<Route_0>;
    "/food": InferRoute<Route_1>;
    "/quiz/[quizId]": InferRoute<Route_2>;
    "/quiz": InferRoute<Route_3>;
  }

  interface StaticRouter {
    "/about/fodmap": StaticRoute;
    "/admin/dashboard": StaticRoute;
    "/": StaticRoute;
    "/under_construction": StaticRoute;
  }
}

TO

declare module "@@@next-typesafe-url" {
  import type { InferRoute, StaticRoute } from "next-typesafe-url";
  import { type RouteType as Route_0 } from "./src/app/food/[foodId]/routeType";
  import { type RouteType as Route_1 } from "./src/app/food/routeType";
  import { type RouteType as Route_2 } from "./src/app/quiz/[quizId]/routeType";
  import { type RouteType as Route_3 } from "./src/app/quiz/routeType";
  interface DynamicRouter {
    "/food/[foodId]": InferRoute<Route_0>;
    "/food": InferRoute<Route_1>;
    "/quiz/[quizId]": InferRoute<Route_2>;
    "/quiz": InferRoute<Route_3>;
  }

  interface StaticRouter {
    "/about/fodmap": StaticRoute;
    "/admin/dashboard": StaticRoute;
    "/": StaticRoute;
    "/under_construction": StaticRoute;
  }
}
lelabo-m commented 6 months ago

Opened a PR (#85) with a proposed fix in case this issue is relevant and not just a project configuration issue.