ゴーグル付きのヘルメットのイラスト

Next.js 型安全なルーティングを使う

Next.js では実験的な機能として、型安全なルーティングを利用できます。この機能を使うことでリンク先のパス名を静的に検査できるため、typo などのエラーを事前に防ぐことができます。

この記事における「型安全」とは、静的な型検査によりランタイムで起こり得るエラーを事前に検知することを指します。

Next.js では Next.js 13.2 より実験的な機能として、型安全なルーティングを利用できます。この機能を使うことでリンク先のパス名を静的に検査できるため、typo などのエラーを事前に防ぐことができます。

なお、型安全なルーティングを利用するためには App Router と TypeScript を使用している必要があります。

型安全なルーティングの利用方法

型安全なルーティングを有効にするためには、experimental.typedRoutes フラグを有効にする必要があります。next.config.mjs に以下のように設定します。

next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    typedRoutes: true,
  },
};
 
export default nextConfig;

ルーティングの型情報は .next/types/link.d.ts ファイルに自動的に生成されます。next devnext build を実行して .next/types ディレクトリが生成されるようにしておきましょう。

next dev
`.next/types/link.d.ts` の型定義

/, /about, /blog/[slug], /shop/[...slug] などのルートを作成した場合以下のような型定義が生成されます。

.next/types/link.d.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 WithProtocol = `${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 =
    | `/`
    | `/about`
  type DynamicRoutes<T extends string = string> =
    | `/blog/${SafeSlug<T>}`
    | `/shop/${CatchAllSlug<T>}`
 
  type RouteImpl<T> =
    | StaticRoutes
    | SearchOrHash
    | WithProtocol
    | `${StaticRoutes}${SearchOrHash}`
    | (T extends `${DynamicRoutes<infer _>}${Suffix}` ? T : never)
 
}
 
declare module 'next' {
  export { default } from 'next/types/index.js'
  export * from 'next/types/index.js'
 
  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.js'
  import type { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
  import type { UrlObject } from 'url'
 
  type LinkRestProps = Omit<
    Omit<
      DetailedHTMLProps<
        AnchorHTMLAttributes<HTMLAnchorElement>,
        HTMLAnchorElement
      >,
      keyof OriginalLinkProps
    > &
      OriginalLinkProps,
    'href'
  >
 
  export type LinkProps<RouteInferType> = 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<RouteInferType> | UrlObject
  }
 
  export default function Link<RouteType>(props: LinkProps<RouteType>): JSX.Element
}
 
declare module 'next/navigation' {
  export * from 'next/dist/client/components/navigation.js'
 
  import type { NavigateOptions, AppRouterInstance as OriginalAppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime.js'
  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;
}

試しに新しいページを作成してみましょう。app/about/page.tsx というファイルを作成します。

mkdir app/about
touch app/about/page.tsx

next/linkLink コンポーネントの href プロパティの型が string から UrlObject | RouteImpl<"">' となっており、/ もしくは /about という文字列のみを受け付けるようになります。

import Link from "next/link";
 
<Link href="/"></Link>; // OK
<Link href="/about"></Link>; // OK
<Link href="/something"></Link>; // Type '"/something"' is not assignable to type 'UrlObject | RouteImpl<"/something">'.
 
// クエリパラメータやハッシュパラメーターを渡せる
<Link href="/about?foo=bar"></Link>;
<Link href="/about#baz"></Link>;
<Link href="/something#?foo=bar"></Link>; // Type '"/something#?foo=bar"' is not assignable to type 'UrlObject | RouteImpl<"/something#?foo=bar">'.
 
// 外部の URL 文字列を受け取る
<Link href="https://example.com/about"></Link>; // OK
<Link href="https://example.com/something"></Link>; // OK
 
// URL オブジェクトの引数の文字列は任意の文字列を渡せるが、エディタの補完が効く
<Link href={new URL("/about")}></Link>; // OK

useRouter フックでも同様に push(), replace(), prefetch() メソッドの引数の型が変更されます。

import { useRouter } from "next/navigation";
 
const router = useRouter();
 
router.push("/"); // OK
router.push("/about"); // OK
router.push("/something"); // Type '"/something"' is not assignable to type 'UrlObject | RouteImpl<"/something">'.

パスパラメータを受け取るダイナミックなルートでは、パスパラメータの部分は任意の文字列として扱われます。app/blog/[slug]/page.tsx というファイルを作成すると、/blog/${string} 形式の型を受け取ることができます。また app/shop/[...slug]/page.tsx のような catch-all ルートにも対応しています。

import Link from "next/link";
 
<Link href="/blog/hello"></Link>; // OK
<Link href="/blog/123"></Link>; // OK
<Link href="/blog/123/456"></Link>; // Type '"/blog/123/456"' is not assignable to type 'UrlObject | RouteImpl<"/blog/123/456">'
<Link href="/about/hello"></Link>; // Type '"/about/hello"' is not assignable to type 'UrlObject | RouteImpl<"/about/hello">'.
<Link href="/shop/aaa/bbb/ccc"></Link>; // OK

ルートパラメータを変数で渡す場合には、テンプレートリテラルを使っている場合には型エラーは発生しません。それ以外の方法で文字列を構築している場合、as Route で型をキャストする必要があります。

import { Route } from "next";
import Link from "next/link";
 
const slug = "hello";
 
<Link href={`/blog/${slug}`}></Link>; // OK`
<Link href={"/blog/" + slug}></Link>; // Type 'string' is not assignable to type 'UrlObject | RouteImpl<string>'.
<Link href={("/blog/" + slug) as Route}></Link>; // OK

<Link> をラップしたコンポーネントを作成し、href Props を渡す場合にはジェネリックを使用します。ルーティングの型は "next" モジュールからインポートした Route を使用します。

import type { Route } from "next";
import Link from "next/link";
 
function MyLink<T extends string>({
  href,
  label,
}: {
  href: Route<T> | URL;
  label: string;
}) {
  return <Link href={href}>{label}</Link>;
}

まとめ

  • Next.js では実験的な機能として、型安全なルーティングを利用できる
  • experimental.typedRoutes フラグを有効にすることで、リンク先のパス名を静的に検査できる
  • ルーティングの型情報は npm run devnpm run build を実行することで app/ ディレクトリのルーティング情報を元に自動生成される -<Link> コンポーネントの href プロパティの型が string から UrlObject | RouteImpl<"">' に変更され、//about, /blog/foo のようなルーティングに対応した文字列のみを受け付けるようになる

参考

記事の理解度チェック

以下の問題に答えて、記事の理解を深めましょう。

Next.js で型安全なルーティングを有効にするため必要な条件として、当てはまらないものはどれか?

  • App Router と TypeScript を使用している

    もう一度考えてみましょう

  • `experimental.typedRoutes` フラグを有効にする

    もう一度考えてみましょう

  • Next.js のバージョンが 13.2 以上である

    もう一度考えてみましょう

  • npm run generate-types コマンドを実行して、型定義ファイルを生成する

    正解!

    型定義ファイルは `npm run dev` や `npm run build` を実行することで自動生成されます。


Contributors

> GitHub で修正を提案する
この記事をシェアする
はてなブックマークに追加

関連記事