【Next.js】ルーティングとSearchParamsを型安全にするライブラリを作った【next-typesafe-path】
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
を使うと、デフォルト値の指定や不正な値のバリデーションを型安全に行えます。
stringOr
、numberOr
、enumOr
などの関数で、想定外の値が紛れ込む事を手軽に防げます。もちろん使用せず組むことも可能です。
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 })
// 長くなってくると辛い..
そんなときこのライブラリ↓
を見て、実装もシンプルになってとても良い!と思ってこのライブラリをインスパイアしたAPIで作り直すことにしました。今後
コードをきれいに書きたい(特に型周り)のと、Next.jsのAPIともっと連携して使いやすくしたいです。
最後に
検索すると同じようなことを実現したいライブラリがいくつも出てくるので、
何か違いを出さなければ...!というマインドになってしまい、リリースやめようかな..とかも思いましたが、なんとか出せました。
そもそもは、Next.js公式のtypedRoutesがSearchParamsに対応すればこのライブラリの役目も終わりますが、どうなんでしょうね?(かなり実装に規則を与える事になるので、そこまでしなかったりするんでしょうか)
ここまで読んでいただきありがとうございました!
Discussion