Skip to content

Commit

Permalink
Merge pull request #9 from ckastbjerg/improve-api
Browse files Browse the repository at this point in the history
Improve api
  • Loading branch information
ckastbjerg authored Apr 7, 2021
2 parents be33004 + 45d1eb0 commit 1955634
Show file tree
Hide file tree
Showing 11 changed files with 145 additions and 56 deletions.
107 changes: 90 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@

With the types generated, you can use the `getRoute` utility to retrieve **links that are guaranteed to exist** in your the application:

```ts
import { getRoute } from "next-type-safe-routes";

getRoute("/users"); // for simple routes
getRoute({ route: "/users/[userId]", userId: "1" }); // for dynamic routes
```
<img src="./getRoute.gif" />

## Features

Expand Down Expand Up @@ -62,7 +57,14 @@ When you start up your application, we will generate types for all of your pages
You can now import the `getRoute` util from `next-type-safe-routes` and use it to retrieve a route that's is guaranteed to exist in your application.

<img src="./getRoute.gif" />
```ts
import { getRoute } from "next-type-safe-routes";

// for simple routes
getRoute("/users");
// for dynamic routes
getRoute({ route: "/users/[userId]", params: { userId: "1" } });
```

Now you just need to decide how you want to integrate `next-type-safe-routes` in your project. If you want inspiration, we demonstrate how to create a simple abstraction for the Next.js `Link` and `router` in [the example project](/example/src).

Expand Down Expand Up @@ -95,29 +97,74 @@ How you ensure that only links to existing pages is essentially up to you, but w

#### The `getRoute` method

Method that converts a type-safe route to an "actual" route:
A simple method that converts a type-safe route to an "actual" route.

First, import the method:

```ts
import { getRoute } from "next-type-safe-routes";
```

For simple (non-dynamic) routes, you can simply do:

```ts
const route = getRoute("/users");
```

This will simply return the string `/users`.

If you need to include a (non-typed) query (or just prefer being more explicit), you can pass an object like so:

```ts
const route = getRoute({
route: "/users",
query: { "not-typed": "whatevs" },
});
```

This will return the string `/users?not-typed=whatevs`.

const route = getRoute("/users"); // => '/users'
const route = getRoute({ route: "/users/[userId]", userId: 1234 }); // => '/users/1234'
```ts
const route = getRoute({
route: "/users/[userId]",
params: { userId: 1234 },
});
```

This will return the string `/users/1234`.

#### The `getPathname` method

Method that just returns the pathname for a type-safe route.
A simple method that just returns the pathname for a type-safe route.

First, import the method:

```ts
import { getPathname } from "next-type-safe-routes";
```

For simple (non-dynamic) routes, you can simply do:

```ts
const path = getPathname("/users");
```

This will return the string `/users`.

And for

```ts
const path = getPathname("/users"); // => '/users'
const path = getPathname({ route: "/users/[userId]", userId: 1234 }); // => '/users/[userId]'
const path = getPathname({
route: "/users/[userId]",
params: { userId: 1234 },
});
```

You may not need this in you application.
This will return the string `/users/[userId]`.

#### The `TypeSafePage` and `TypeSafeApiRoute` types

These are useful for making your own abstraction. For instance, if you want to make a tiny abstraction ontop of the `next/router`:
These can be useful for making your own abstraction. For instance, if you want to make a tiny abstraction ontop of the `next/router`:

```ts
import { TypeSafePage, getRoute } from "next-type-safe-routes";
Expand All @@ -126,6 +173,7 @@ import { useRouter as useNextRouter } from "next/router";
const useRouter = () => {
const router = useNextRouter();

// Say you only want to allow links to pages (and not API routes)
const push = (typeSafeUrl: TypeSafePage) => {
router.push(getRoute(typeSafeUrl));
};
Expand All @@ -136,12 +184,37 @@ const useRouter = () => {
export default useRouter;
```

The type can be of the type `string` (for non-dynamic routes) or `{ route: string, >ROUTE_PARAMS< }` for dynamic routes. For instance:
For basic routes, the type can be of the type `string` or:

```ts
{
route: string,
query?: { ... } // any key value pairs (not type-safe)
}
```

And for dynamic routes, the type is always:

```ts
{
route: string,
params: { ... }, // based on the file name
query?: { ... } // any key value pairs (not type-safe)
}
```

**Example**:

```ts
type Query = { [key: string]: string | number };
export type TypeSafePage =
| "/users"
| { route: "/users/[userId]"; userId: string | string[] | number };
| { route: "/users"; query?: Query }
| {
route: "/users/[userId]";
params: { userId: string | number };
query?: Query;
};
```

> Note, the `TypeSafePage` and `TypeSafeApiRoute` are kept separate even though they are essentially the same type. We do this, as you may potentially want to distinguish between them in your application.
Expand Down
Binary file modified example.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 4 additions & 7 deletions example/src/@types/next-type-safe-routes/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
/* eslint no-use-before-define: 0 */ // --> OFF
// prettier-ignore

// IMPORTANT! This file is autogenerated by the `type-safe-next-routes`
// package. You should _not_ update these types manually...

declare module "next-type-safe-routes" {
export type TypeSafePage = "/" | { route: "/users/[userId]", userId: string | string[] | number } | "/users";
export type TypeSafeApiRoute = "/api/mocks" | { route: "/api/users/[userId]", userId: string | string[] | number } | "/api/users";

type Query = { [key: string]: string | number };
export type TypeSafePage = "/" | { route: "/", query?: Query } | { route: "/users/[userId]", params: { userId: string | string[] | number }, query?: Query } | "/users" | { route: "/users", query?: Query };
export type TypeSafeApiRoute = "/api/mocks" | { route: "/api/mocks", query?: Query } | { route: "/api/users/[userId]", params: { userId: string | string[] | number }, query?: Query } | "/api/users" | { route: "/api/users", query?: Query };
export const getPathname = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => string;
export const getRoute = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute, query?: any) => string;
export const getRoute = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => string;
}
6 changes: 5 additions & 1 deletion example/src/pages/users/[userId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { useApiRoute, useRouter } from "hooks";
const UserPage = () => {
const { query } = useRouter();
const userId = query.userId as string;
const [user] = useApiRoute({ route: "/api/users/[userId]", userId });
const [user] = useApiRoute({
route: "/api/users/[userId]",
params: { userId },
query: { test: 2 },
});

if (!user) {
return "Loading...";
Expand Down
4 changes: 3 additions & 1 deletion example/src/pages/users/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ const UsersPage = () => {
return (
<>
{users.map(({ userId, name }) => (
<Link to={{ route: "/users/[userId]", userId }}>{name}</Link>
<Link to={{ route: "/users/[userId]", params: { userId } }}>
{name}
</Link>
))}
</>
);
Expand Down
Binary file modified getRoute.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-type-safe-routes",
"version": "0.1.0",
"version": "0.2.0-alpha",
"description": "Never should your users experience broken links again!",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ exports[`plugin/generateTypeScriptFile works as expected 1`] = `
// package. You should _not_ update these types manually...
declare module \\"next-type-safe-routes\\" {
export type TypeSafePage = \\"/404\\" | \\"/\\" | { route: \\"/users/[userId]\\", userId: string | string[] | number } | \\"/users\\";
export type TypeSafeApiRoute = { route: \\"/api/[authId]\\", authId: string | string[] | number } | { route: \\"/api/users/[userId]\\", userId: string | string[] | number } | \\"/api/users\\";
type Query = { [key: string]: string | number };
export type TypeSafePage = \\"/404\\" | { route: \\"/404\\", query?: Query } | \\"/\\" | { route: \\"/\\", query?: Query } | { route: \\"/users/[userId]\\", params: { userId: string | string[] | number }, query?: Query } | \\"/users\\" | { route: \\"/users\\", query?: Query };
export type TypeSafeApiRoute = { route: \\"/api/[authId]\\", params: { authId: string | string[] | number }, query?: Query } | { route: \\"/api/users/[userId]\\", params: { userId: string | string[] | number }, query?: Query } | \\"/api/users\\" | { route: \\"/api/users\\", query?: Query };
export const getPathname = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => string;
export const getRoute = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute, query?: any) => string;
export const getRoute = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => string;
}
"
`;
14 changes: 8 additions & 6 deletions src/plugin/generateTypeScriptFile/getFileContent.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { ApiRoute, Page } from "./types";

const getParam = (param: string) => `${param}: string | string[] | number`;

const getTypeSafeRoute = ({ route, params }: ApiRoute) => {
if (!params?.length) {
return `"${route}"`;
return `"${route}" | { route: "${route}", query?: Query }`;
}

return `{ route: "${route}", ${params
.map((param) => `${param}: string | string[] | number`)
.join(",")} }`;
const paramsString = params.map(getParam).join(",");

return `{ route: "${route}", params: { ${paramsString} }, query?: Query }`;
};

type Args = {
Expand All @@ -22,6 +24,7 @@ const getFileContent = ({
// package. You should _not_ update these types manually...
declare module "next-type-safe-routes" {
type Query = { [key: string]: string | number };
export type TypeSafePage = ${pages.map(getTypeSafeRoute).join(" | ")};
${
apiRoutes.length > 0
Expand All @@ -30,9 +33,8 @@ declare module "next-type-safe-routes" {
.join(" | ")};`
: ""
}
export const getPathname = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => string;
export const getRoute = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute, query?: any) => string;
export const getRoute = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => string;
}
`;

Expand Down
23 changes: 16 additions & 7 deletions src/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@ describe("utils/getRoute", () => {
});

it("works as expected when having (untyped) query params", () => {
expect(getRoute("/routes", { a: "b" })).toBe("/routes?a=b");
expect(getRoute({ route: "/routes", query: { a: "b", 1: 2 } })).toBe(
"/routes?1=2&a=b"
);
});

it("works as expected for dynamic routes", () => {
const route = getRoute({ route: "/routes/[routeId]", routeId: 1 });
const route = getRoute({
route: "/routes/[routeId]",
params: { routeId: 1 },
});
expect(route).toBe("/routes/1");
});

it("works as expected when having (untyped) query params for dynamic routes", () => {
const route = getRoute(
{ route: "/routes/[routeId]", routeId: 1 },
{ a: "b" }
);
const route = getRoute({
route: "/routes/[routeId]",
params: { routeId: 1 },
query: { a: "b" },
});
expect(route).toBe("/routes/1?a=b");
});
});
Expand All @@ -29,7 +35,10 @@ describe("utils/getPathname", () => {
});

it("works as expected for dynamic routes", () => {
const route = getPathname({ route: "/routes/[routeId]", routeId: 1 });
const route = getPathname({
route: "/routes/[routeId]",
params: { routeId: 1 },
});
expect(route).toBe("/routes/[routeId]");
});
});
26 changes: 14 additions & 12 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// NOTE, these will be replaced with the "real" TypeSafePage type
// when generating types for a project
type TypeSafePage = string | { route: string; routeId: string | number };
type TypeSafeApiRoute = string | { route: string; routeId: string | number };
type Query = { [key: string]: string | number };
type TypeSafePage =
| string
| { route: string; query?: Query }
| { route: string; params: any; query?: Query };
type TypeSafeApiRoute = TypeSafePage;

export const getPathname = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => {
if (typeof typeSafeUrl === "string") {
Expand All @@ -20,20 +24,18 @@ const getSearchParams = (query: any) => {
return `?${params}`;
};

export const getRoute = (
typeSafeUrl: TypeSafePage | TypeSafeApiRoute,
query?: any
) => {
const searchParams = getSearchParams(query);
export const getRoute = (typeSafeUrl: TypeSafePage | TypeSafeApiRoute) => {
if (typeof typeSafeUrl === "string") {
return `${typeSafeUrl}${searchParams}`;
return `${typeSafeUrl}`;
}

const { route, ...params } = typeSafeUrl;
let href = route as string;
const searchParams = getSearchParams(typeSafeUrl.query);

let route = typeSafeUrl.route as string;
const params = "params" in typeSafeUrl ? typeSafeUrl.params : {};
Object.keys(params).forEach((param) => {
href = href.replace(`[${param}]`, (params as any)[param]);
route = route.replace(`[${param}]`, (params as any)[param]);
});

return `${href}${searchParams}`;
return `${route}${searchParams}`;
};

0 comments on commit 1955634

Please sign in to comment.