壊れていないコードに出た型エラーが、指していたのは設計の穴でした

ポートフォリオとして制作中の自作アプリ「Hubpin」(Next.js 16 + Supabase)で、
同じ日に2回、同じ形の型エラーに当たりました。

型エラーが出ると、まず「今のコードのどこが壊れているのか」を探しにいきます。
ところがこの2件は、探しても壊れている場所が見つかりませんでした。動きます。
それでも赤線が出る。

最初はどちらも、型を黙らせる方向に手が動きました。
書き終わってから見直すと、エラーが指していたのは別のところでした。

目次

TypeScript は「proxy が守っている」ことを知りません

/dashboard は proxy で保護しています。
Next.js 16 で middleware から改名されたファイルで、リクエストがページに届く前に走ります。
ここを通っている時点でログイン済みなので、中では素直にこう書けるはずでした。

const { data } = await supabase.auth.getClaims()
const email = data.claims.email  // ← ここが型エラー

datanull になりうるので怒られます。

でも実行時に null になることはありません。その手前で proxy が弾いているからです。

TypeScript はそのことを知りません。
そして、知らないのが正しいと後から分かりました。

proxy がどのパスで動くかは matcher という設定が決めていて、これは1行の配列です。
この1行を書き換えたら、さっきのコードは本当に落ちます。

実際、同じ工程で matcher を絞りすぎてセッション更新が止まり、
1時間後にログアウトされる事故を踏んでいます。
守られている前提は、自分で思っているほど固定されていませんでした。

型が指していたのは、proxy が外れた未来のほう。
今このコードが動くかどうかは、はじめから見ていませんでした。

?. を付ける場所も間違えていました

最初はこう書いて逃げようとしました。

const email = (await supabase.auth.getClaims())?.data.claims.email

これは守る場所が違います。
getClaims() は必ず { data, error } の形で返ってくるので、この位置は null になりません。
null になるのは data のほうです。

const email = data?.claims.email

同じ記号でも、置く場所を1つずらすと何も守らなくなります。
このときは「エラーが消えたから合っている」で通しかけました。

戻り値を受け取る人がいませんでした

ログインフォームは3段階に分けて作っていました。
まず HTML のフォームと、中身が空の Server Action。
次に Supabase の認証を繋ぐ。最後にエラーを画面に出す。

型エラーが出たのは、2段階目を終えたところでした。
失敗したときにエラーメッセージを返す形に変えたら、<form> に渡せなくなりました。

Type '(formData: FormData) => Promise<{ message: string; }>'
  is not assignable to type '(formData) => void | Promise<void>'

<form action={...}> が受け取れるのは、文字列か、戻り値を持たない関数だけです。
{ message: string } を返す関数は、そこに入りません。

ここでも型を通す方向に手が動きました。戻り値を消せばエラーは消えます。

ただ、消したらエラーメッセージが画面に出せません。
返した値をどこかで受け取って表示する仕組みを、まだ作っていませんでした。

3段階目でやる予定だった useActionState を、そこで入れました。
戻り値を受け取って表示する経路ができます。
すると、Server Action 側を1文字も書き換えていないのにエラーが消えました。

TypeScript が言っていたのは「その戻り値、受け取る人がいませんよ」でした。
足りなかったのは型の知識ではなく、受け取り手のほう。

2件に共通していたもの

並べてみると、指していたものが違うだけで形は同じでした。

エラーが指していたもの
proxy と getClaims 将来の壊れ方(proxy を外したら落ちる)
Server Action の戻り値 足りない設計(戻り値の行き先がない)

どちらも「今のコードの誤り」ではありませんでした。
そして2件とも、最初に手が動いたのは黙らせる方向でした。

?. を足す。戻り値を消す。どちらも赤線は消えます。
消えたあとに残るのは、proxy を外したら落ちるコードと、
エラーを画面に出せないフォームです。

おわりに

2件は別々の手順で出ています。
ログインフォームを作る手順が目安1.0時間に対して実績4.8時間、
proxy を作る手順が2.2時間でした。

丸写しなら、どちらももっと短く終わっていたと思います。
時間を使ったのは、赤線を消す前に「これは何を言っているのか」で止まった部分でした。

型エラーを「今の間違い」だと思って読むと、消す方向に手が動きます。
「将来こう壊れる」「この設計が足りない」と読めると、直し方が変わりました。
自分は2回とも、最初は消す方向に動いています。

次は、そもそもエラーが出ないまま間違っているケースの話を書きます。
今回のように赤線が出るのは、まだ親切なほうでした。

目次