How to get Next.js internal links in a type-safe way
Tags: typescript, pathpida
Published: 2020-12-28 03:43:41

## Problem

There is Link that transitions to `/post/1`.

```tsx
import Link from 'next/link'

export default () => {
  const url = `/post/${1}`
  return <Link href={url} />
}
```

Cannot statically type-check if `/post/${pid}` is correct as an internal link.
If the file `pages/post/[pid].tsx` does not exist, the page transition will fail.

## Solution

[Pathpida](https://github.com/aspida/pathpida) solves this problem.
Reads `pages/` directory in Next.js and automatically generates a type definition file for getting internal links.

![pathpida logo](https://dev-to-uploads.s3.amazonaws.com/i/wes9e2zanyuxnpkih4mn.png)

For example, if `pages/` directory is in the following state

```
pages/[pid].tsx
pages/blog/[...slug].tsx
pages/index.tsx
```

the following TypeScript file will be generated automatically.

`lib/$path.ts`

```ts
export const pagesPath = {
  _pid: (pid: number | string) => ({
    $url: () => ({ pathname: '/[pid]', query: { pid }})
  }),
  blog: {
    _slug: (slug: string[]) => ({
      $url: () => ({ pathname: '/blog/[...slug]', query: { slug }})
    })
  },
  $url: () => ({ pathname: '/' })
}
```

As you can see, `pagesPath` property corresponds to a file in `pages/` directory.
The object returned by `$url` method can be passed directly to `next/link` and `next/router`.

In the component, you can use it as follows.

```tsx
import Link from 'next/link'
import { pagesPath } from '../lib/$path'

export default () => {
  return <Link href={pagesPath.blog._slug(['a', 'b', 'c']).$url()} />
}
```

## Usage in Next.js

This explanation assumes that Next.js and TypeScript have been set up beforehand.

Install `pathpida` and `npm-run-all`.

```sh
$ yarn add pathpida npm-run-all --dev
```

`package.json`

```json
{
  "scripts": {
    "dev": "run-p dev:*",
    "dev:next": "next dev",
    "dev:path": "pathpida --watch",
    "build": "pathpida && next build"
  }
}
```

Start Next.js and pathpida in monitoring mode with Dev command.

```sh
$ yarn dev
```

`$path.ts` file will be automatically created in either `utils/` or `lib/` directory.

```
lib/
  $path.ts
pages/
  _app.tsx
  index.tsx
```

`lib/$path.ts`

```ts
export const pagesPath = {
  $url: () => ({ pathname: '/' })
}
```

If you add more files to `pages/` directory, `lib/$path.ts` file will be changed.

`pagesPath` can be imported and used with `next/link` or `next/router`.

```
lib/
  $path.ts
pages/
  articles/
    [id].tsx
  users/
    [...userInfo].tsx
  _app.tsx
  index.tsx
```

`components/ActiveLInk.tsx`

```tsx
import Link from 'next/link'
import { useRouter } from 'next/router'
import { pagesPath } from '../lib/$path'

function ActiveLink() {
  const router = useRouter()

  const handleClick = () => {
    router.push(pagesPath.users._userInfo(['mario', 'hello', 'world!']).$url())
  }

  return <>
    <div onClick={handleClick}>Hello</div>
    <Link href={pagesPath.articles._id(1).$url()}>
      World!
    </Link>
  </>
}

export default ActiveLink
```

## How to specify a query

You can specify a query just by exporting `Query` type in `pages/` component.
For example, to create `/user?userId={number}`, write the following

`pages/user.tsx`

```tsx
export type Query = {
  userId: number
}

export default () => <div />
```

To set a query in another component, write the following

```ts
import { pagesPath } from '../lib/$path'

pagesPath.user.$url({ query: { userId: 1 }})
```

## How to specify a hash

Just pass hash as an argument of $url method with any string you like.
`#` will be given automatically.

```ts
import { pagesPath } from '../lib/$path'

pagesPath.user.$url({ query: { userId: 1 }, hash: 'hoge' })
pagesPath.user.$url({ hash: 'fuga' })
```

## You can also get the file path of `public/` directory

If you pass `--enableStatic` option to pathpida command, it will read the contents of `public/` directory and generate a staticPath client.

`package.json`

```json
{
  "scripts": {
    "dev": "run-p dev:*",
    "dev:next": "next dev",
    "dev:path": "pathpida --enableStatic --watch",
    "build": "pathpida --enableStatic && next build"
  }
}
```

If you have JSON and PNG images in `public/`

```
public/aa.json
public/bb/cc.png

lib/$path.ts or utils/$path.ts
```

`staticPath` client can get the path string directly instead of `$url` method.

`pages/index.tsx`

```tsx
import Link from 'next/link'
import { staticPath } from '../lib/$path'

console.log(staticPath.aa_json) // /aa.json

export default () => <img src={staticPath.bb.cc_png} />
```

## Thanks for reading to the end

I'd appreciate it if you could give me a star on GitHub!

{% github aspida/pathpida %}