🎉

【Next.js】ルーティングとSearchParamsを型安全にするライブラリを作った【next-typesafe-path】

2025/03/09に公開

next-typesafe-path

https://github.com/Uki884/next-typesafe-path

まだまだですが、良いなと思ったらスターもらえたら嬉しいです><

使い方

# npm
npm install next-typesafe-path
# yarn
yarn add next-typesafe-path
# pnpm
pnpm add next-typesafe-path

あとはpackage.jsonにスクリプトを登録しておきます。

"dev": "npm run dev:next & npm run dev:path",
"dev:next": "next dev",
"dev:path": "next-typesafe-path --watch",

また、npxつかった起動も可能です。

# npx
npx next-typesafe-path

ルートディレクトリにすべてのルートの型定義が自動生成されます。たったこれだけで使い始められます!

ルートパスの型安全な生成

$path 関数を使うと、Next.jsのルーティングのパスを型安全に取得できます。

import { $path } from "next-typesafe-path";

// シンプルなパス
const route1 = $path("/about/"); // "/about/"

// ダイナミックルート
const route2 = $path("/users/[user_id]/", { userId: 1 }); // "/users/1/"

// 可変長パス + クエリパラメータ
const route3 = $path(
  "/products/[[...filters]]", 
  { filters: ["hoge", "fuga"] }, 
  { sort: "asc", page: 1 }
); 
// "/products/hoge/fuga?sort=asc&page=1"

型安全なSearchParamsの管理

通常のNext.jsでは、searchParamsオブジェクト や router.query でURLのクエリを取得できますが、型安全ではありません。

createSearchParams を使うと、デフォルト値の指定や不正な値のバリデーションを型安全に行えます。

stringOrnumberOrenumOrなどの関数で、想定外の値が紛れ込む事を手軽に防げます。もちろん使用せず組むことも可能です。

test/page.tsx

import { createSearchParams, InferSearchParams, parseSearchParams } from "next-typesafe-path";

const SearchParams = createSearchParams(
  ({ numberOr, enumOr, stringOr }) => ({
    page: numberOr(1), // url中にpageがなかったらデフォルトの1になる
    sort: enumOr(["asc", "desc"] as const, "desc"), // デフォルトはdesc
    q: stringOr(""), // デフォルトは空文字
  })
)

export type SearchParams = InferSearchParams<typeof SearchParams>;

const parsedSearchParams = parseSearchParams(SearchParams, await searchParams);

// /test
// { page: 1, sort: 'desc', q: '' }

デフォルトではcreateSearchParamsに指定してないパラメーターがURLに入っててもパースできますが、passthroughオプションをfalseに設定することで未知のパラメーターを取り除けます。

const parsedSearchParams = parseSearchParams(SearchParams, query, { passthrough: false });

// /test?hoge=fuga&page=2
// { page: 2, sort: 'desc', q: '' }

全ページ共通のSearchParamsの管理(任意)

極稀にアプリケーション共通で同じSearchParamsをつけたい場合ってあると思います。
setGlobalSearchParamsを使います。

next-typesafe-path.config.tsという名前のファイルをルートディレクトリ(デフォルト)に配置します。

next-typesafe-path.config.ts

import { createSearchParams, InferSearchParams, setGlobalSearchParams } from "next-typesafe-path";

export const searchParams = createSearchParams((p) => ({
  from: p.enumOr(['header', 'footer', 'top'], 'top'),
}));

export type SearchParams = InferSearchParams<typeof searchParams>;

setGlobalSearchParams(searchParams);

そしてファイルの先頭で読み込みます。

app/layout.tsx

import "../next-typesafe-path.config";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

これで$pathで型が効き、parseSearchParamsしたときにグローバルなクエリが入るようになります。

また別の使い方として、passthrough()を追加することで、型を緩めて自由にキーを$pathに渡せるようにできます。

next-typesafe-path.config.ts

export const searchParams = createSearchParams(() => ({})).passthrough();

test/page.tsx

import { createSearchParams, InferSearchParams, parseSearchParams } from "next-typesafe-path";

const SearchParams = createSearchParams(
  ({ numberOr, enumOr, stringOr }) => ({
    page: numberOr(1),
    sort: enumOr(["asc", "desc"] as const, "desc"),
    q: stringOr(""),
  })
)

$path('/test/', { hoge: 'fuga' }) // エラーにならない

モチベーション

型安全ルーティング自体は、Next.js公式のtypedRoutesなども出てきていて、めずらしいものではありませんし、似たようなライブラリが何個も出てきていますが、一番は、業務で作ったルーティングのパスを自動生成するライブラリをちゃんとリリースしたかったからです。(せっかくなので)

また、日本でメジャーな pathpida が直感的じゃないなぁと感じてたからです。

例えば、pathpidaの特徴として生成された関数を使ってこのように使います。

console.log(pagesPath.post._pid(1).$url()); // { pathname: '/post/[pid]', query: { pid: 1 }}

一見してURLとしては可読性があまり高くなく、また クエリパラメータの必須/任意を分ける必要があり、認知負荷が高い と感じたことで使用を断念しました。(今探せばもっと良い選択肢あったかも)

// クエリが必須の場合
export type Query = {
  limit: number;
  label?: string;
};

// クエリを任意にしたい場合
export type OptionalQuery = {
  limit: number;
  label?: string;
};

あとは、もともと業務で作ってたものは、URLを結合して一つの関数にすることを考えていたんですが、やっぱりURLと1対1で対応していないので視認性が悪いなぁと思ってました。

/users -> users()
/users/:id -> usersId({ id: 1 })
/users/:userId/posts/:postId -> usersIdPostsId({ userId: 1, postId: 1 })
// 長くなってくると辛い..

そんなときこのライブラリ↓
https://github.com/yesmeck/safe-routes
を見て、実装もシンプルになってとても良い!と思ってこのライブラリをインスパイアしたAPIで作り直すことにしました。

今後

コードをきれいに書きたい(特に型周り)のと、Next.jsのAPIともっと連携して使いやすくしたいです。

最後に

検索すると同じようなことを実現したいライブラリがいくつも出てくるので、
何か違いを出さなければ...!というマインドになってしまい、リリースやめようかな..とかも思いましたが、なんとか出せました。

そもそもは、Next.js公式のtypedRoutesがSearchParamsに対応すればこのライブラリの役目も終わりますが、どうなんでしょうね?(かなり実装に規則を与える事になるので、そこまでしなかったりするんでしょうか)

ここまで読んでいただきありがとうございました!

Discussion