Nextjsで型安全なルーティングを楽しむ

## はじめに
NextjsでApp Routerを用いた開発を行なっていますか？
App Routerは[React Canaries](https://react.dev/blog/2023/05/03/react-canaries)から提供されるサーバー側コンポーネントとクライアント側コンポーネントを利用したコンポーネント単位でレンダリング方法を制御する機能や、直感的なルーティング、そのほかにも豊富な機能を持つNextjsの新し機能です。

そんなApp Routerですがアプリケーション内のルーティングに対して型を付与する`Statically Typed Links`という機能が[バージョン13.2](https://nextjs.org/blog/next-13-2#statically-typed-links)からbeta版で提供がされています。
Tanstack Routerの[registering-router-types](https://tanstack.com/router/v1/docs/guide/routes#registering-router-types)やVue Routerの[typed-routes](https://router.vuejs.org/guide/advanced/typed-routes.html)と似たような機能です(何もbeta版です)。

この記事ではそんな`Statically Typed Links`の使い方について紹介します。

## Statically Typed Linksを有効化する
`Statically Typed Links`はbeta版の機能なので`next.config.js`で有効にすることで利用できます。

```js:next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    typedRoutes: true,
  },
};
 
module.exports = nextConfig;
```
TypeScriptを用いた型提供なので、TypeScriptを用いたアプリケーションのみで有効なことに注意してください。

## Statically Typed Linksによる型付け
Stacically Typed Linksを有効にするとNextjsはApp Routerのディレクトリ構成をもとにルーティングに関する様々な型を生成してくれます。
型は`next dev`と`next build`を実行するタイミングで`.next/types/link.d.ts`に作られます(`next dev`は実行中であればルーティングの変更に合わせて自動でファイルを書き換えます)。
`create-next-app`から生成したばかりの状態では以下のように生成されます。

```ts
// Type definitions for Next.js routes

/**
 * Internal types used by the Next.js router and Link component.
 * These types are not meant to be used directly.
 * @internal
 */
declare namespace __next_route_internal_types__ {
  type SearchOrHash = `?${string}` | `#${string}`

  type Suffix = '' | SearchOrHash

  type SafeSlug<S extends string> = S extends `${string}/${string}`
    ? never
    : S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type CatchAllSlug<S extends string> = S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type OptionalCatchAllSlug<S extends string> =
    S extends `${string}${SearchOrHash}` ? never : S

  type StaticRoutes = 
    | `/`
  type DynamicRoutes<T extends string = string> = never

  type RouteImpl<T> = 
    | StaticRoutes
    | `${StaticRoutes}${SearchOrHash}`
    | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)
    
}

declare module 'next' {
  export { default } from 'next/types'
  export * from 'next/types'

  export type Route<T extends string = string> =
    __next_route_internal_types__.RouteImpl<T>
}

declare module 'next/link' {
  import type { LinkProps as OriginalLinkProps } from 'next/dist/client/link'
  import type { AnchorHTMLAttributes } from 'react'
  import type { UrlObject } from 'url'
  
  type LinkRestProps = Omit<
    Omit<AnchorHTMLAttributes<HTMLAnchorElement>, keyof OriginalLinkProps> &
      OriginalLinkProps,
    'href'
  >

  export type LinkProps<T> = LinkRestProps & {
    /**
     * The path or URL to navigate to. This is the only required prop. It can also be an object.
     * @see https://nextjs.org/docs/api-reference/next/link
     */
    href: __next_route_internal_types__.RouteImpl<T> | UrlObject
  }

  export default function Link<RouteType>(props: LinkProps<RouteType>): JSX.Element
}

declare module 'next/navigation' {
  export * from 'next/dist/client/components/navigation'

  import type { NavigateOptions, AppRouterInstance as OriginalAppRouterInstance } from 'next/dist/shared/lib/app-router-context'
  interface AppRouterInstance extends OriginalAppRouterInstance {
    /**
     * Navigate to the provided href.
     * Pushes a new history entry.
     */
    push<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void
    /**
     * Navigate to the provided href.
     * Replaces the current history entry.
     */
    replace<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>, options?: NavigateOptions): void
    /**
     * Prefetch the provided href.
     */
    prefetch<RouteType>(href: __next_route_internal_types__.RouteImpl<RouteType>): void
  }

  export declare function useRouter(): AppRouterInstance;
}
```

ディレクトリ構成をもとにしたルーティング可能なパスを表す文字列の型を作成し、それをもとにNextjsが持つルーティングに関するAPIに対して型付けをしています。よく使うものだと`Link`コンポーネントの`href`、`useRouter`の`push`や`prefetch`などの型を上書きしています。

ディレクトリをもとにしたルーティングは`__next_route_internal_types__`で組み上げています。
```ts
declare namespace __next_route_internal_types__ {
  type SearchOrHash = `?${string}` | `#${string}`

  type Suffix = '' | SearchOrHash

  type SafeSlug<S extends string> = S extends `${string}/${string}`
    ? never
    : S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type CatchAllSlug<S extends string> = S extends `${string}${SearchOrHash}`
    ? never
    : S extends ''
    ? never
    : S

  type OptionalCatchAllSlug<S extends string> =
    S extends `${string}${SearchOrHash}` ? never : S

  type StaticRoutes = 
    | `/`
  type DynamicRoutes<T extends string = string> = never

  type RouteImpl<T> = 
    | StaticRoutes
    | `${StaticRoutes}${SearchOrHash}`
    | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)   
}
```
この部分が型安全なルーティングを組み上げる根幹なので3つのパートに分けて紐解いて行きます。
### ディレクトリ構成によって形を変える型
ディレクトリ構成に依存して変化する部分は`StaticRoutes`と`DynamicRoutes`だけです。
`StaticRoutes`はとりうる静的なパスが、`DynamicRoutes`はとりうる動的なパスがそれぞれユニオンで記述されます。
`/`と`/accounts`と`/accounts/:id`のようなパスにルーティングされている場合は以下のようになります。
```ts
type StaticRoutes = 
  | `/`
  | `/accounts`
type DynamicRoutes<T extends string = string> = 
  | `/accounts/${SafeSlug<T>}`
```
`SafeSlug<T>`の部分は後ほど解説しますが、動的パスの部分の確かさを検証しています。
### ルーティング型を表現するための型
`SearchOrHash`はパスのうち、検索パラメータやハッシュの部分を表す型です。
```ts
type SearchOrHash = `?${string}` | `#${string}`
```
`Suffix`は`SearchOrHash`に加えて空文字列をユニオンで取った型です。
```ts
type Suffix = '' | SearchOrHash
```
`SafeSlug`は`DynamicRoutes`の動的な部分を検査する時に利用する型です。文字列がスラッシュを含まず、後尾に検索パラメータやハッシュを含まないことと空文字列ではないことを確認します。
```ts
type SafeSlug<S extends string> = S extends `${string}/${string}`
  ? never
  : S extends `${string}${SearchOrHash}`
  ? never
  : S extends ''
  ? never
  : S
```
`CatchAllSlug`と`OptionalCatchAllSlug`は`SafeSlug`と同じく、`DynamicRoutes`の動的な部分を検査する時に利用する型です。名前の通り動的ルーティングのうち[Catch All Segment](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#catch-all-segments)と[Optional Catch All Options](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments)のような階層が不明な動的なパスを取る場合に使われます。
```ts
type DynamicRoutes<T extends string = string> = 
  | `/accounts/${CatchAllSlug<T>}`
  | `/users/${OptionalCatchAllSlug<T>}`
```
`CatchAllSlug`は`SafeSlug`からスラッシュが含まれないことを確認する部分を排除し、`OptionalCatchAllSlug`はそれに加えて空文字列ではないことを確認する部分を排除した型となっています。
```ts
type CatchAllSlug<S extends string> = S extends `${string}${SearchOrHash}`
  ? never
  : S extends ''
  ? never
  : S

type OptionalCatchAllSlug<S extends string> =
  S extends `${string}${SearchOrHash}` ? never : S
```
### ルーティングパス
これまでみてきた型を組み合わせて実際にルーティングが提供するパスを`RouteImpl`で表現しています。
```ts
type RouteImpl<T> = 
  | StaticRoutes
  | `${StaticRoutes}${SearchOrHash}`
  | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)  
```
静的なパス`StaticRoutes`と静的なパス`StaticRoutes`に検索パラメータやハッシュ`SearchOrHash`を足したもので静的なルーティングパスを表現しています。一見`${StaticRoutes}${Suffix}`で良さそうな気もしますが、このように表現した場合パスの入力補完が効かないため分けて記述されています([こちらのコメント](https://github.com/vercel/next.js/blob/498692b69812a096178eaabfea766f4d8fa82413/packages/next/src/build/webpack/plugins/next-types-plugin/index.ts#L395C10-L395C62)を参照)。
最後の
```ts
T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never
```
は型に当てはめる文字列が動的なパス`DynamicRoutes`に`Suffix`を加えた形式の文字列であれば自身を返し、そうでなければ`never`を返すことで擬似的にルーティングパスを表現しています。`DynamicRoutes`に渡す`T`はパス文字列ではなく動的パラメータであることに注意してください。静的なルーティングのようにシンプルなリテラル型で表現できないので自動保管は効きません。

### ルーティングされた機能
先程まで見てきた`RouteImpl`を使ってNextjsの機能が型付けされていきます。
ルーティング可能な文字列として`next`からインポートできる`Route`という型で定義されています。
```ts
export type Route<T extends string = string> =
  __next_route_internal_types__.RouteImpl<T>
```
そして、`next/link`から利用する`Link`コンポーネントの`href`は以下のように定義されています。
```ts
href: __next_route_internal_types__.RouteImpl<T> | UrlObject
```
先程まで見てきた`RouteImpl`と`UrlObject`のどちらかを取る型を持ちます。
`UrlObject`を利用可能にすると型の安全性が担保できないので、生成した型のみでルーティングさせるコンポーネントを作ってみます。

```tsx
import { Route } from 'next';
import Link from 'next/link';
import { ReactNode } from 'react';

export const Anchor = <T extends string>({
  href,
  children,
}: {
  href: Route<T>;
  children: ReactNode;
}) => {
  return (
    <Link href={href}>
      {children}
    </Link>
  );
};
```
`Route`が提供されているので、簡単に型安全なルーティングパスに則った型を用いたコンポーネントを作れます。

`useRouter`のそれぞれのメソッドにもパスを渡す引数に対してルーティングに基づいた型が定義されています。
```ts
href: __next_route_internal_types__.RouteImpl<RouteType>
```

ディレクトリによってルーティングしたパスが`/`、`/accounts`、`/accounts/:id`の場合は型をチェックは以下のような結果になります。
```tsx
// OK
<Link href="/" />
<Link href="/accounts" />
<Link href="/accounts/1" />
<Link href={`/accounts/${id}`} />
<Link href={('/accounts/' + id) as Route} />
navigate.push('/');
navigate.replace('/accounts');
 
// NG
<Link href="/aboot" />
<Link href={'/accounts/' + id} />
navigate.prefetch('/accounts' + id);
```

## おわりに
この記事では`Statically Typed Links`を有効にしてNextjsで型安全なルーティングを行う方法を紹介しました。
`Statically Typed Links`ではこれまで型として検査できなかった部分が検査可能になり、より安全で快適なアプリケーションを構築できるようになります。beta版の機能ですが、大きなバグ等はなかったのでやむを得ない事情が出ない限りは有効にしてはいかがでしょうか。