🌑

Next.jsの15.5でstableになった「Typed Routes」を使って<Link href='/hoge'>を型安全に使う

に公開

はじめに

今日はNext.jsの15.5がリリースされて「Typed Routes」がstableになったので、自分のサービスにも適用していきたいと思います。

https://nextjs.org/blog/next-15-5#typescript-improvements

この前作った「this is OOO」で使ってみます。

https://zenn.dev/shinaps/articles/5806c6579c5aeb

まずはNext.js関連のライブラリを最新バージョンに更新します。

pnpm install next@latest react@latest react-dom@latest

このプロジェクトはOpenNextで動かしてるので@opennextjs/cloudflareも関係しているため、これも更新して、pnpm run previewで動くか確認します。

ログ
pnpm run preview

> thisisooo@0.1.0 preview /Users/shinaps/dev/thisisooo
> opennextjs-cloudflare build && opennextjs-cloudflare preview --env development


┌─────────────────────────────┐
│ OpenNext — Cloudflare build │
└─────────────────────────────┘

App directory: /Users/shinaps/dev/thisisooo
Next.js version : 15.5.0
@opennextjs/cloudflare version: 1.6.5
@opennextjs/aws version: 3.7.4

┌─────────────────────────────────┐
│ OpenNext — Building Next.js app │
└─────────────────────────────────┘


> thisisooo@0.1.0 build /Users/shinaps/dev/thisisooo
> next build

   ▲ Next.js 15.5.0
   - Environments: .env.production, .env
   - Experiments (use with caution):
     · serverActions

Using vars defined in .dev.vars
   Creating an optimized production build ...
Using vars defined in .dev.vars
Using vars defined in .dev.vars
Using vars defined in .dev.vars
 ✓ Compiled successfully in 8.6s
 ✓ Linting and checking validity of types    
 ✓ Collecting page data    
Using vars defined in .dev.vars
 ✓ Generating static pages (13/13)
 ✓ Collecting build traces    
 ✓ Finalizing page optimization    

Route (app)                                   Size  First Load JS    
┌ ƒ /                                      3.07 kB         120 kB
├ ○ /_not-found                              993 B         103 kB
├ ƒ /api/auth/[...all]                       131 B         102 kB
├ ○ /apple-icon.jpeg                           0 B            0 B
├ ƒ /articles                                167 B         105 kB
├ ƒ /articles/[articleId]                  68.1 kB         182 kB
├ ƒ /articles/[articleId]/opengraph-image    131 B         102 kB
├ ƒ /articles/[articleId]/twitter-image      131 B         102 kB
├ ○ /icon.jpeg                                 0 B            0 B
├ ƒ /interviews                            4.37 kB         121 kB
├ ƒ /interviews/[interviewId]              6.52 kB         133 kB
├ ○ /opengraph-image.jpg                       0 B            0 B
├ ƒ /profile                               4.76 kB         147 kB
├ ○ /sign-in                               2.28 kB         122 kB
└ ○ /twitter-image.jpg                         0 B            0 B
+ First Load JS shared by all               102 kB
  ├ chunks/150-db3db4cf74aef2d5.js         45.3 kB
  ├ chunks/cccb8abb-197eef17418c575f.js    54.2 kB
  └ other shared chunks (total)            1.93 kB


ƒ Middleware                               34.5 kB

○  (Static)   prerendered as static content
ƒ  (Dynamic)  server-rendered on demand


┌──────────────────────────────┐
│ OpenNext — Generating bundle │
└──────────────────────────────┘

Bundling middleware function...
Bundling static assets...
Bundling cache assets...
Building server function: default...
Applying code patches: 1.923s
# copyPackageTemplateFiles
⚙️ Bundling the OpenNext server...

Worker saved in `.open-next/worker.js` 🚀

OpenNext build complete.

┌───────────────────────────────┐
│ OpenNext — Cloudflare preview │
└───────────────────────────────┘

Populating R2 incremental cache...
Using vars defined in .dev.vars
100%|█████████████████████████████████████████████| 9/9 [00:04:<00:00:, 2.12it/s]
Successfully populated cache with 9 assets
Tag cache does not need populating

 ⛅️ wrangler 4.29.0 (update available 4.32.0)
─────────────────────────────────────────────
Using vars defined in .dev.vars
Your Worker has access to the following bindings:
Binding                         Resource                Mode
env.KV                          KV Namespace            local
  preview-kv
env.D1                          D1 Database             local
  d1
env.NEXT_INC_CACHE_R2_BUCKET    R2 Bucket               local
  thisisooo-next-incremental-cache-dev
env.WORKER_SELF_REFERENCE       Worker                  local [not connected]
  thisisooo
env.ASSETS                      Assets                  local
env.NEXTJS_ENV                  Environment Variable    local
  "(hidden)"


Service bindings, Durable Object bindings, and Tail consumers connect to other `wrangler dev` processes running locally, with their connection status indicated by [connected] or [not connected]. For more details, refer to https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/#local-development

╭──────────────────────────────────────────────────────────────────────╮
│  [b] open a browser [d] open devtools [c] clear console [x] to exit  │
╰──────────────────────────────────────────────────────────────────────╯
⎔ Starting local server...
[wrangler:info] ✨ Parsed 1 valid header rule.
[wrangler:info] Ready on http://localhost:8787

問題なく動きました。

Typed Routes

やることはnext.config.tstypedRoutes: trueを追加するだけです。

const nextConfig = {
  typedRoutes: true, // Now stable!
};
 
export default nextConfig;

補完機能は微妙(エディタによるのかもしれない)ですが、

無効なページへのパスを入れると型エラーがちゃんと出ました。

Route Props Helpers

「Typed Routes」によってPageProps, LayoutProps, RouteContextについても型チェックが効くようになったそうなので試してみました。

The system automatically discovers routes from your file structure, supporting dynamic routes, parallel routes, and custom routes from next.config.js. Type generation runs in both development and build modes, immediately regenerating types when your file structure changes in development, and scales efficiently to large projects by generating only a few optimized files instead of the many individual files used in the previous implementation.

と記載されている通り、ファイルの構造から自動で検知してくれるみたいですね。

LayoutProps

今までは

RootLayout(props: { children: ReactNode; header: ReactNode })

としていたところを、

RootLayout(props: LayoutProps<'/'>)

と書き換えました。

補完も効きます。

PageProps

今までは

InterviewPage({ params }: { params: Promise<{ interviewId: string }> })

としていたところを、

InterviewPage({ params }: PageProps<'/interviews/[interviewId]'>)

と書き換えました。


RouteContextはsrc/app/api/auth/[...all]/route.tsしか使ってなくて、これもbetter-authの設定なので特にやることはありませんでした。

感想

嬉しいかも〜!

Discussion