Skip to content

Commit

Permalink
remove global type pollution (#23)
Browse files Browse the repository at this point in the history
* remove global type pollution

* v0.0.15

* update readme

* type refinements

* Update CHANGELOG.md
  • Loading branch information
tatethurston authored Jul 7, 2022
1 parent a201a73 commit 7bf7868
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 83 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## 0.0.15

- nextjs-routes no longer adds types to the global type namespace. Previously,
`Route` was available globally. Now, it must be imported:

```ts
import type { Route } from "nextjs-routes";
```

- query from `useRouter` is now correctly typed as `string | undefined` instead of `string`. If you know the current route, you can supply a type argument to narrow required parameters to `string`, eg:

```
// if you have a page /foos/[foo].ts
const router = useRouter<"/foos/[foo]">();
// foo will be typed as a string, because the foo query parameter is required and thus will always be present.
const { foo } = router.query;
```

## 0.0.14

- Allow passing in `query` without `pathname` to change current url parameters.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,14 @@ import { useRouter } from "next/link";
const { query } = useRouter<"/foos/[foo]">();
```

### Route

If you want to use the generated `Route` type in your code, you can import it from `nextjs-routes`:

```ts
import type { Route } from "nextjs-routes";
```

## Highlights

🦄 Zero config
Expand Down
36 changes: 21 additions & 15 deletions e2e/nextjs-routes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,23 @@
// Run `yarn nextjs-routes` to regenerate this file.
/* eslint-disable */

type Route =
| { pathname?: "/foos/[foo]"; query: Query<{ foo: string }> }
| { pathname?: "/"; query?: Query | undefined };
declare module "nextjs-routes" {
export type Route =
| { pathname: "/foos/[foo]"; query: Query<{ foo: string }> }
| { pathname: "/"; query?: Query | undefined };

type Query<Params = {}> = Params & {
[key: string]: string;
};

type Pathname = Exclude<Route["pathname"], undefined>;

type QueryForPathname = {
[K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
};
type Query<Params = {}> = Params & { [key: string]: string | undefined };
}

declare module "next/link" {
import type { Route } from "nextjs-routes";
import type { LinkProps as NextLinkProps } from "next/dist/client/link";
import type { PropsWithChildren, MouseEventHandler } from "react";

type RouteOrQuery = Route | { query?: { [key: string]: string | undefined } };

export interface LinkProps extends Omit<NextLinkProps, "href"> {
href: Route;
href: RouteOrQuery;
}

declare function Link(
Expand All @@ -40,23 +37,32 @@ declare module "next/link" {
}

declare module "next/router" {
import type { Route } from "nextjs-routes";
import type { NextRouter as Router } from "next/dist/client/router";
export { RouterEvent } 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 | undefined } };

export interface NextRouter<P extends Pathname = Pathname>
extends Omit<Router, "push" | "replace"> {
pathname: P;
route: P;
query: QueryForPathname[P];
push(
url: Route,
url: RouteOrQuery,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
replace(
url: Route,
url: RouteOrQuery,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
Expand Down
60 changes: 52 additions & 8 deletions e2e/typetest.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from "next/link";
import { useRouter } from "next/router";
import { useRouter, RouterEvent } from "next/router";
import type { Route } from "nextjs-routes";

// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
function expectType<T>(_value: T) {}
Expand All @@ -11,6 +12,7 @@ function expectType<T>(_value: T) {}
<Link href={{ pathname: "/", query: undefined }} />;
<Link href={{ pathname: "/", query: {} }} />;
<Link href={{ pathname: "/", query: { bar: "baz" } }} />;
<Link href={{ pathname: "/", query: { bar: undefined } }} />;

// Path with dynamic segments
<Link href={{ pathname: "/foos/[foo]", query: { foo: "baz" } }} />;
Expand Down Expand Up @@ -47,15 +49,19 @@ function expectType<T>(_value: T) {}
const router = useRouter();

// pathname

expectType<"/" | "/foos/[foo]">(router.pathname);

// route

expectType<"/" | "/foos/[foo]">(router.route);

// query
expectType<{ foo: string; [key: string]: string } | { [key: string]: string }>(
router.query
);

expectType<string | undefined>(router.query.foo);
expectType<string | undefined>(router.query.bar);
// type narrowing
expectType<string>(useRouter<"/foos/[foo]">().query.foo);

// push

Expand All @@ -80,9 +86,13 @@ router.push({ query: { bar: "baz" } });
router.push({ query: { foo: "foo" } });

// Unaugmented options
router.push({}, undefined, { shallow: true, locale: "en", scroll: true });
router.push({ query: {} }, undefined, {
shallow: true,
locale: "en",
scroll: true,
});
// @ts-expect-error shallow typo
router.push({}, undefined, { shallowy: true });
router.push({ query: {} }, undefined, { shallowy: true });

// replace

Expand All @@ -107,6 +117,40 @@ router.replace({ query: { bar: "baz" } });
router.replace({ query: { foo: "foo" } });

// Unaugmented options
router.replace({}, undefined, { shallow: true, locale: "en", scroll: true });
router.replace({ query: {} }, undefined, {
shallow: true,
locale: "en",
scroll: true,
});
// @ts-expect-error shallow typo
router.replace({}, undefined, { shallowy: true });
router.replace({ query: {} }, undefined, { shallowy: true });

// RouterEvent

let routerEvent: RouterEvent;

routerEvent = "routeChangeStart";
// @ts-expect-error event typo
routerEvent = "routeChangeStarty";

// nextjs-routes

// Route

let route: Route;

// Path without dynamic segments
route = { pathname: "/" };
route = { pathname: "/", query: undefined };
route = { pathname: "/", query: {} };
route = { pathname: "/", query: { bar: "baz" } };

// Path with dynamic segments
route = { pathname: "/foos/[foo]", query: { foo: "baz" } };
// @ts-expect-error missing 'foo' in query
route = { pathname: "/foos/[foo]", query: { bar: "baz" } };
// @ts-expect-error missing 'foo' in query
route = { pathname: "/foos/[foo]", query: undefined };
// @ts-expect-error missing 'foo' in query
route = { pathname: "/foos/[foo]", query: {} };
route = { pathname: "/foos/[foo]", query: { foo: "baz", bar: "baz" } };
2 changes: 1 addition & 1 deletion public.package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nextjs-routes",
"version": "0.0.14",
"version": "0.0.15",
"description": "Type safe routing for Next.js",
"license": "MIT",
"author": "Tate <[email protected]>",
Expand Down
76 changes: 43 additions & 33 deletions src/__snapshots__/test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,39 @@ exports[`route generation typescript 1`] = `
// Run \`yarn nextjs-routes\` to regenerate this file.
/* eslint-disable */
type Route =
| { pathname?: '/404'; query?: Query | undefined }
| { pathname?: '/[foo]'; query: Query<{ foo: string; }> }
| { pathname?: '/[foo]/[bar]/[baz]'; query: Query<{ foo: string;bar: string;baz: string; }> }
| { pathname?: '/[foo]/bar/[baz]'; query: Query<{ foo: string;baz: string; }> }
| { pathname?: '/[foo]/bar/[baz]/foo/[bar]'; query: Query<{ foo: string;baz: string;bar: string; }> }
| { pathname?: '/[foo]/baz'; query: Query<{ foo: string; }> }
| { pathname?: '/_debug/health-check'; query?: Query | undefined }
| { pathname?: '/_error'; query?: Query | undefined }
| { pathname?: '/api/[[...segments]]'; query?: Query | undefined }
| { pathname?: '/api/[...segments]'; query: Query<{ segments: string[]; }> }
| { pathname?: '/api/bar'; query?: Query | undefined }
| { pathname?: '/foo/[slug]'; query: Query<{ slug: string; }> }
| { pathname?: '/'; query?: Query | undefined }
| { pathname?: '/not-found'; query?: Query | undefined }
| { pathname?: '/settings/bars/[bar]'; query: Query<{ bar: string; }> }
| { pathname?: '/settings/bars/[bar]/baz'; query: Query<{ bar: string; }> }
| { pathname?: '/settings/foo'; query?: Query | undefined }
| { pathname?: '/settings'; query?: Query | undefined }
declare module \\"nextjs-routes\\" {
export type Route =
| { pathname: '/404'; query?: Query | undefined }
| { pathname: '/[foo]'; query: Query<{ foo: string; }> }
| { pathname: '/[foo]/[bar]/[baz]'; query: Query<{ foo: string;bar: string;baz: string; }> }
| { pathname: '/[foo]/bar/[baz]'; query: Query<{ foo: string;baz: string; }> }
| { pathname: '/[foo]/bar/[baz]/foo/[bar]'; query: Query<{ foo: string;baz: string;bar: string; }> }
| { pathname: '/[foo]/baz'; query: Query<{ foo: string; }> }
| { pathname: '/_debug/health-check'; query?: Query | undefined }
| { pathname: '/_error'; query?: Query | undefined }
| { pathname: '/api/[[...segments]]'; query?: Query | undefined }
| { pathname: '/api/[...segments]'; query: Query<{ segments: string[]; }> }
| { pathname: '/api/bar'; query?: Query | undefined }
| { pathname: '/foo/[slug]'; query: Query<{ slug: string; }> }
| { pathname: '/'; query?: Query | undefined }
| { pathname: '/not-found'; query?: Query | undefined }
| { pathname: '/settings/bars/[bar]'; query: Query<{ bar: string; }> }
| { pathname: '/settings/bars/[bar]/baz'; query: Query<{ bar: string; }> }
| { pathname: '/settings/foo'; query?: Query | undefined }
| { pathname: '/settings'; query?: Query | undefined }
type Query<Params = {}> = Params & {
[key: string]: string;
type Query<Params = {}> = Params & { [key: string]: string | undefined };
}
type Pathname = Exclude<Route[\\"pathname\\"], undefined>;
type QueryForPathname = {
[K in Route as K[\\"pathname\\"]]: Exclude<K[\\"query\\"], undefined>;
};
declare module \\"next/link\\" {
import type { Route } from \\"nextjs-routes\\";
import type { LinkProps as NextLinkProps } from \\"next/dist/client/link\\";
import type { PropsWithChildren, MouseEventHandler } from \\"react\\";
type RouteOrQuery = Route | { query?: { [key: string]: string | undefined } };
export interface LinkProps extends Omit<NextLinkProps, \\"href\\"> {
href: Route;
href: RouteOrQuery;
}
declare function Link(
Expand All @@ -59,25 +56,38 @@ declare module \\"next/link\\" {
}
declare module \\"next/router\\" {
import type { Route } from \\"nextjs-routes\\";
import type { NextRouter as Router } from \\"next/dist/client/router\\";
export { RouterEvent } from \\"next/dist/client/router\\";
type TransitionOptions = Parameters<Router[\\"push\\"]>[2];
export interface NextRouter<P extends Pathname = Pathname> extends Omit<Router, \\"push\\" | \\"replace\\"> {
type Pathname = Route[\\"pathname\\"];
type QueryForPathname = {
[K in Route as K[\\"pathname\\"]]: Exclude<K[\\"query\\"], undefined>;
};
type RouteOrQuery = Route | { query: { [key: string]: string | undefined } };
export interface NextRouter<P extends Pathname = Pathname>
extends Omit<Router, \\"push\\" | \\"replace\\"> {
pathname: P;
route: P;
route: P;
query: QueryForPathname[P];
push(url: Route, as?: string, options?: TransitionOptions): Promise<boolean>;
push(
url: RouteOrQuery,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
replace(
url: Route,
url: RouteOrQuery,
as?: string,
options?: TransitionOptions
): Promise<boolean>;
}
export function useRouter<P extends Pathname>(): NextRouter<P>;
}
"
`;
Loading

0 comments on commit 7bf7868

Please sign in to comment.