8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.jsのTypeScriptエラー:非同期params・入力検証・lintを分けて直す

8
Last updated at Posted at 2025-03-10

2026-10-12 訂正:Promise・generic・イベント型・JSXの欠落、入力全体のDB保存、JSON不正の扱い、lint回避を訂正しました。複数プロジェクトでの本人経験は確認できていないため撤回し、仕様確認と独立した入力検証例として記載します。

Nextの変更とTypeScriptの既存規則を分ける

Next.js 15更新ガイドが示すAsync Request APIsにはparams/searchParams等があります。これは単なる型チェック厳格化ではなくAPIの変更です。15の一時的な同期アクセス互換を完成した修正と扱わず、対象APIと呼び出し側を移行します。

一方、引数が暗黙anyになる指摘はTypeScriptのnoImplicitAnyという既存の設定に由来します。すべての型エラーをNext.js 15が新しく導入した問題とは説明できません。採用するNext/React/TypeScriptと型package・lintの互換を確認します。実行時TypeErrorとコンパイル時の型エラーも区別します。

paramsとmetadataに型引数を戻す

以下はapp/posts/[slug]/page.tsx用の構文例です。実API取得を装わず、slugを表示するだけにしています。JSXを含むファイルは.tsxです。

import type { Metadata } from 'next'
type Props = { params: Promise<{ slug: string }> }

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  return { title: `記事 ${slug}` }
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params
  return <main><h1>記事 {slug}</h1></main>
}

旧Promiseだけの注釈には型引数が欠けていました。組織ルートならPromise<{ orgId: string }>のようにURLの形へ合わせます。実データ取得では取得関数の戻り値を定義し、識別子・認可・不在と障害を扱います。例外メッセージだけで原因を決めず、stackとAPI変更、取得結果を確認します。awaitが必要な値を確認し、無関係な同期処理まですべて非同期化しません。

API型とJSONは別の境界

success時のdataとfailure時のerrorを必須にするdiscriminated unionの例です。Tを宣言し、Promiseへ具体的な型を指定します。

export type ApiResponse<T> =
  | { success: true; data: T }
  | { success: false; error: string }

export type UserDto = {
  id: string
  name: string
  createdAt: string
}

export async function readJson(response: Response): Promise<unknown> {
  if (!response.ok) throw new Error('HTTP応答が失敗しました')
  const value: unknown = await response.json()
  return value
}

createdAtをJSONで送るなら通常はISO文字列です。DBのDate型とDTOを同一視せず、変換と有効性を検証します。readJsonはHTTP失敗を扱いますがUserDtoの形はまだ保証しません。呼出側はschema等でunknownを検証してから扱います。as UserDtoや戻り値の注釈だけではAPI応答のruntime検証になりません。

POSTは検証済みの項目だけを保存する

旧db.users.create({ data: body })はrole等の未許可項目を保存し得るため撤回しました。必須項目がtruthyかだけでは型・長さ・email等を確認できません。不正JSONをserver障害と混ぜず、処理段階ごとに判断します。

次はZod 3.25.76を使って確認した独立例です。アプリではimport先を実際の依存へ合わせます。APIの認証・DB処理を実装済みとは扱いません。

const { z } = require('zod');
const createUserSchema = z.object({
  name: z.string().trim().min(1).max(100),
  email: z.string().trim().email().max(254)
}).strict();
function parseUserInput(raw) {
  let value;
  try { value = JSON.parse(raw); }
  catch { return { ok: false, status: 400 }; }
  const result = createUserSchema.safeParse(value);
  if (!result.success) return { ok: false, status: 422 };
  return { ok: true, data: { name: result.data.name, email: result.data.email } };
}

12項目で正常入力・trim・元入力保持、不正JSON、null/配列/欠落、role追加、空白/非string/長すぎるname、不正/長すぎるemailを確認しました。Zodのemail検査はアドレスの所有や到達性の証明ではありません。

Route Handlerへ接続する時は、認証・対象tenantの認可を先に設計し、bodyの容量制限・rate limit・採用するcookie認証のCSRF対策を確認します。上の例は不正JSONを400、schema不適合を422に分ける方針です。保存は検証したname/emailとserverが決めるrole/tenantだけへ限定し、DBのunique制約と競合、再送・結果不明、transactionを扱います。保存失敗は入力不正へ変換せず、秘密情報を応答へ出しません。

GETも認可済みtenant/権限の範囲へ制限します。queryの長さ、limitの上限、pagination、取得項目、個人情報を定義し、無制限のusers一覧を公開しません。旧dbはimport/定義も省略されたplaceholderでした。今回、実DBや認証を操作していません。

フォームとButtonの型・JSX

以下は入力イベントの型を絞る例です。currentTargetを使い、HTMLInputElementのvalueへアクセスできます。

'use client'
import { useState } from 'react'
import type { ChangeEvent, ReactNode } from 'react'

export function NameField() {
  const [name, setName] = useState('')
  function handleChange(event: ChangeEvent<HTMLInputElement>) {
    setName(event.currentTarget.value)
  }
  return <label>名前<input name="name" value={name} onChange={handleChange} /></label>
}

type ButtonProps = {
  children: ReactNode
  onClick: () => void
  disabled?: boolean
}
export function Button({ children, onClick, disabled = false }: ButtonProps) {
  return <button type="button" onClick={onClick} disabled={disabled}>{children}</button>
}

textarea/selectと共有するならChangeEvent等、実際の対象を含めます。nameから任意のstate keyを書き換える設計なら許可されたkeyを検証します。フォーム送信はFormEvent等を使い、応答の受付結果を確認します。HTTP成功だけで業務完了とせず、失敗確定・結果不明を分け、入力保持・重複防止を扱います。

旧Cardのimage &&だけの式やtags.mapの戻り値なし、タグが欠落した例は撤回しました。必要なら実際のimg/Imageとalt、mapのreturnとkey、callbackを接続します。型定義を足すだけで表示・interactionが実装されるわけではありません。

Zodから型を抽出する場合はtype ContactFormData = z.inferのようにschemaを指定します。旧z.inferだけの記載を訂正しました。schemaとUI型をそろえてもserver側の再検証は必要です。

lintを止めることは型修正ではない

未使用importを削除し、引数・props・generic・nullable値を直してから再検査します。no-unused-varsをwarn/offへ変える、ignoreDuringBuildsを使うという旧方法は検査ゲートの変更であり、型問題そのものの解決として推奨しません。

Next.js 16更新ガイドではnext lintとNext設定のeslint optionが削除され、next buildはlintを実行しません。15の設定を16へコピーせず、lintを別コマンドとしてCIで実行します。ESLintのflat configとTypeScriptの検査は別で、互換性のあるeslint-config-next等へ合わせます。

strictやnoUncheckedIndexedAccess等は意図した設定として導入し、発見された主要分岐を修正します。既存tsconfigを置き換えてNextが必要とする設定を失ったり、型assertionで実データの問題を隠したりしません。

今回確認したのは公式の版別変更、掲載コードの構文、独立DTO例のstrict型検査、Zod入力12項目です。Next/React全体の型検査・build・lint、browserフォーム操作、認証・DB・実APIは未実行です。

この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?