Repository: tatethurston/nextjs-routes
Pull request #69 (2022-09-25 23:04:14)

Export `QueryForPathname`
(I promise this is the last PR I create today lol)

Just adding this type export so that one can easily type queries in NextJS data fetching functions (e.g.: `GetStaticProps`).

The use case I'm considering is something like:
```
// In blogs/[id].tsx

import type { GetStaticProps } from 'next';
import type { QueryForPathname } from 'next/router';

export const getStaticProps: GetStaticProps<{}, QueryForPathname['/blogs/[id]']> = async (context) => {
    const { id } = context.params; // This will be typed! :)
}
```

tatethurston: Awesome, thanks for the contribution and outlining the use case.
tatethurston: We'll want to export the type from the block

```
declare module "nextjs-routes" {
```

instead of from:

```
declare module "next/router" {
```

And add type tests to `e2e/typetest.tsx`.

What do you think about naming this `Query`? Something like:

```ts
type Query<Pathname extends Route["pathname"] = Route["pathname"]> = Extract<
  Route,
  { pathname: Pathname }
>['query'];
```

MariaSolOs: @tatethurston Sounds good, I'll move the export and update the tests.

The `nextjs-routes` module already has a type named `Query`. What about `RoutedQuery`? That makes it clear that this is the typed query object of a specific route.
tatethurston: Yeah that sounds great
MariaSolOs: @tatethurston QQ: How am I supposed to update the `nextjs-routes.d.ts` file in the `e2e` package? It seems like it has been previously generated by the CLI but isn't that getting deprecated? 
tatethurston: Just mergeed https://github.com/tatethurston/nextjs-routes/pull/70, you can `npm test` to regenerate this file.

Alternatively `next build` or `next dev` in the e2e directory will regenerate the file.

Patch:
diff --git a/README.md b/README.md
index ac6740a..d61d33f 100644
--- a/README.md
+++ b/README.md
@@ -149,6 +149,29 @@ import type { Route } from "nextjs-routes";
 
 `nextjs-routes` generates types for the `pathname` and `query` for every page in your `pages` directory. The generated types are written to `nextjs-routes.d.ts` which is automatically referenced by your Next project's `tsconfig.json`. `nextjs-routes.d.ts` redefines the types for `next/link` and `next/router` and applies the generated route types.
 
+## Typed queries in static generation and server-side rendering
+
+`nextjs-routes` exports the utility generic type `RoutedQuery` so that you can type the context parameter inside `getStaticProps` and `getServerSideProps`:
+
+```ts
+import type { GetStaticProps } from "next";
+import type { RoutedQuery } from "nextjs-routes";
+
+type FooPageProps = { bar: string };
+
+export const getStaticProps: GetStaticProps<
+  FooPageProps,
+  RoutedQuery<"/foos/[foo]">
+> = (context) => {
+  // foo will be correctly typed to a string
+  const { foo } = context.params!;
+
+  return {
+    props: { foo },
+  };
+};
+```
+
 ## What if I need a runtime?
 
 There are some cases where you may want to generate a type safe path from a `Route` object, such as when `fetch`ing from an API route or serving redirects from `getServerSideProps`. These accept `strings` instead of the `Route` object that `Link` and `useRouter` accept. Because these do not perform the same string interpolation for dynamic routes, runtime code is required instead of a type only solution.
diff --git a/examples/src-pages/nextjs-routes.d.ts b/examples/src-pages/nextjs-routes.d.ts
index c36bad2..5b1939d 100644
--- a/examples/src-pages/nextjs-routes.d.ts
+++ b/examples/src-pages/nextjs-routes.d.ts
@@ -10,8 +10,16 @@ declare module "nextjs-routes" {
     | { pathname: "/foos/[foo]"; query: Query<{ "foo": string }> }
     | { pathname: "/"; query?: Query | undefined };
 
+  export type Pathname = Route["pathname"];
+
   type Query<Params = {}> = Params & { [key: string]: string | string[] | undefined };
 
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Pathname> = QueryForPathname[P];
+
   /**
    * A typesafe utility function for generating paths in your application.
    *
@@ -50,26 +58,20 @@ declare module "next/link" {
 
 // prettier-ignore
 declare module "next/router" {
-  import type { Route } from "nextjs-routes";
+  import type { Route, Pathname, RoutedQuery } from "nextjs-routes";
   import type { NextRouter as Router } from "next/dist/client/router";
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
   type TransitionOptions = Parameters<Router["push"]>[2];
 
-  type Pathname = Route["pathname"];
-
-  type QueryForPathname = {
-    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
-  };
-
   type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Pathname = Pathname>
     extends Omit<Router, "push" | "replace"> {
     pathname: P;
     route: P;
-    query: QueryForPathname[P];
+    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
diff --git a/examples/src-pages/src/pages/bars/[bar].tsx b/examples/src-pages/src/pages/bars/[bar].tsx
index 0b329a9..591c27c 100644
--- a/examples/src-pages/src/pages/bars/[bar].tsx
+++ b/examples/src-pages/src/pages/bars/[bar].tsx
@@ -1,9 +1,20 @@
-import { NextPage } from "next";
-import { useRouter } from "next/router";
+import type { NextPage, GetStaticProps } from "next";
+import type { RoutedQuery } from "nextjs-routes";
 
-const Bar: NextPage = () => {
-  const router = useRouter<"/bars/[bar]">();
-  const { bar } = router.query;
+type BarPageProps = { bar: string };
+
+export const getStaticProps: GetStaticProps<
+  BarPageProps,
+  RoutedQuery<"/bars/[bar]">
+> = (context) => {
+  const { bar } = context.params!;
+
+  return {
+    props: { bar },
+  };
+};
+
+const Bar: NextPage<BarPageProps> = ({ bar }) => {
   return <div>Bar: {bar}</div>;
 };
 
diff --git a/src/core.ts b/src/core.ts
index d01df21..cc204d9 100644
--- a/src/core.ts
+++ b/src/core.ts
@@ -115,8 +115,16 @@ declare module "nextjs-routes" {
             .join("\n    | ")}`
     };
 
+  export type Pathname = Route["pathname"];
+
   type Query<Params = {}> = Params & ${unknownQueryParamsType};
 
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Pathname> = QueryForPathname[P];
+
   /**
    * A typesafe utility function for generating paths in your application.
    *
@@ -155,26 +163,20 @@ declare module "next/link" {
 
 // prettier-ignore
 declare module "next/router" {
-  import type { Route } from "nextjs-routes";
+  import type { Route, Pathname, RoutedQuery } from "nextjs-routes";
   import type { NextRouter as Router } from "next/dist/client/router";
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
   type TransitionOptions = Parameters<Router["push"]>[2];
 
-  type Pathname = Route["pathname"];
-
-  type QueryForPathname = {
-    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
-  };
-
   type RouteOrQuery = Route | { query: ${unknownQueryParamsType} };
 
   export interface NextRouter<P extends Pathname = Pathname>
     extends Omit<Router, "push" | "replace"> {
     pathname: P;
     route: P;
-    query: QueryForPathname[P];
+    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,


Test patch:
diff --git a/e2e/nextjs-routes.d.ts b/e2e/nextjs-routes.d.ts
index 5eeb572..2507d65 100644
--- a/e2e/nextjs-routes.d.ts
+++ b/e2e/nextjs-routes.d.ts
@@ -1,5 +1,5 @@
 // THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-// Run `npx nextjs-routes` to regenerate this file.
+// This file will be automatically regenerated when your Next.js server is running.
 /* eslint-disable */
 
 // prettier-ignore
@@ -8,8 +8,16 @@ declare module "nextjs-routes" {
     | { pathname: "/foos/[foo]"; query: Query<{ "foo": string }> }
     | { pathname: "/"; query?: Query | undefined };
 
+  export type Pathname = Route["pathname"];
+
   type Query<Params = {}> = Params & { [key: string]: string | string[] | undefined };
 
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Pathname> = QueryForPathname[P];
+
   /**
    * A typesafe utility function for generating paths in your application.
    *
@@ -48,26 +56,20 @@ declare module "next/link" {
 
 // prettier-ignore
 declare module "next/router" {
-  import type { Route } from "nextjs-routes";
+  import type { Route, Pathname, RoutedQuery } from "nextjs-routes";
   import type { NextRouter as Router } from "next/dist/client/router";
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
   type TransitionOptions = Parameters<Router["push"]>[2];
 
-  type Pathname = Route["pathname"];
-
-  type QueryForPathname = {
-    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
-  };
-
   type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Pathname = Pathname>
     extends Omit<Router, "push" | "replace"> {
     pathname: P;
     route: P;
-    query: QueryForPathname[P];
+    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
diff --git a/e2e/typetest.tsx b/e2e/typetest.tsx
index 6f59742..99fbbbf 100644
--- a/e2e/typetest.tsx
+++ b/e2e/typetest.tsx
@@ -1,7 +1,7 @@
 import Link from "next/link";
 import { LinkProps } from "next/link";
 import { useRouter, RouterEvent, NextRouter } from "next/router";
-import { route, type Route } from "nextjs-routes";
+import { route, type Route, type RoutedQuery } from "nextjs-routes";
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
 function expectType<T>(_value: T) {}
@@ -201,3 +201,20 @@ route({ pathname: "/foos/[foo]", query: { foo: "baz" } });
 route({ pathname: "/foos/[foo]", query: { bar: "baz" } });
 // @ts-expect-error 'foo' must be a string, not string[]
 route({ pathname: "/foos/[foo]", query: { foo: ["bar", "baz"] } });
+
+// RoutedQuery
+// Path without dynamic segments
+let rq1: RoutedQuery<"/">;
+rq1 = {};
+rq1 = { foo: "baz" };
+rq1 = { foo: ["bar", "baz"] };
+rq1 = { foo: undefined };
+// Path with dynamic segments
+let rq2: RoutedQuery<"/foos/[foo]">;
+// @ts-expect-error missing 'foo' in query
+rq2 = {};
+rq2 = { foo: "bar" };
+// @ts-expect-error missing 'foo' in query
+rq2 = { bar: "baz" };
+// @ts-expect-error 'foo' must be a string, not string[]
+rq2 = { foo: ["bar", "baz"] };
diff --git a/src/__snapshots__/core.test.ts.snap b/src/__snapshots__/core.test.ts.snap
index 7484b77..a3a25ec 100644
--- a/src/__snapshots__/core.test.ts.snap
+++ b/src/__snapshots__/core.test.ts.snap
@@ -13,8 +13,16 @@ declare module "nextjs-routes" {
   export type Route =
     never;
 
+  export type Pathname = Route["pathname"];
+
   type Query<Params = {}> = Params & { [key: string]: string | string[] | undefined };
 
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Pathname> = QueryForPathname[P];
+
   /**
    * A typesafe utility function for generating paths in your application.
    *
@@ -53,26 +61,20 @@ declare module "next/link" {
 
 // prettier-ignore
 declare module "next/router" {
-  import type { Route } from "nextjs-routes";
+  import type { Route, Pathname, RoutedQuery } from "nextjs-routes";
   import type { NextRouter as Router } from "next/dist/client/router";
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
   type TransitionOptions = Parameters<Router["push"]>[2];
 
-  type Pathname = Route["pathname"];
-
-  type QueryForPathname = {
-    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
-  };
-
   type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Pathname = Pathname>
     extends Omit<Router, "push" | "replace"> {
     pathname: P;
     route: P;
-    query: QueryForPathname[P];
+    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
@@ -122,8 +124,16 @@ declare module "nextjs-routes" {
     | { pathname: "/settings/foo"; query?: Query | undefined }
     | { pathname: "/settings"; query?: Query | undefined };
 
+  export type Pathname = Route["pathname"];
+
   type Query<Params = {}> = Params & { [key: string]: string | string[] | undefined };
 
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Pathname> = QueryForPathname[P];
+
   /**
    * A typesafe utility function for generating paths in your application.
    *
@@ -162,26 +172,20 @@ declare module "next/link" {
 
 // prettier-ignore
 declare module "next/router" {
-  import type { Route } from "nextjs-routes";
+  import type { Route, Pathname, RoutedQuery } from "nextjs-routes";
   import type { NextRouter as Router } from "next/dist/client/router";
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
   type TransitionOptions = Parameters<Router["push"]>[2];
 
-  type Pathname = Route["pathname"];
-
-  type QueryForPathname = {
-    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
-  };
-
   type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Pathname = Pathname>
     extends Omit<Router, "push" | "replace"> {
     pathname: P;
     route: P;
-    query: QueryForPathname[P];
+    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,