転職用のポートフォリオとして、個人でWebアプリ「Hubpin」を作っています。
制作の経緯は「Hubpin 開発記録」に時系列でまとめています。
設計と判断は自分で行い、実装も原則自分で書いています。AIには手順書・レビュー・調査を任せています。
前回、: Item と書いた型注釈が誰にも検証されていなかった話を書きました。
その続きです。
CRUDを書き始める前に、supabase gen types でデータベースから型を生成して、
手書きしていた型を置き換えました。
これで注釈が検証される側に回るはずでした。
tsc を走らせたら、エラーがちょうど1件出ました。
弱くなったのはカードの種類でした
error TS2322
Types of property 'type' are incompatible.
Type 'string' is not assignable to type '"link" | "note" | "feed"'.
カードの種類を表す type というカラムです。
| 型 | |||
|---|---|---|---|
| 手書きしていた型 | `”link” \ | “note” \ | “feed”` |
| 生成された型 | string |
生成型のほうが弱い。
順序が逆だと思いました。
実物から作った型のほうが、手で書いた型より広くなっています。
生成型が保証しているのはカラム構成でした
理由はデータベース側を見たら分かりました。
type text not null check (type in ('link', 'note', 'feed')),
カラムの型は text です。
値を3つに限っているのは、その後ろの check という制約のほうでした。
生成される型が持っているのは「そのテーブルにどんなカラムがあるか」で、
「その列にどんな値が入りうるか」までは含まれていません。
だから text は string になります。
ここは書き方によって変わります。
Postgres には値の範囲そのものを型として定義する enum があって、
そちらを使っていれば生成型にもユニオンとして落ちてきます。
text に check を付ける書き方だと、制約の情報は型まで届きません。
同じ「3つに限る」でも、写り方が違いました。
つまり tsc が出した1件は、型が食い違っているという報告ではありませんでした。
手書きの型が、データベースの保証していない範囲まで約束していた。
そのずれが、置き換えた瞬間に1件のエラーとして出てきただけでした。
手書きの型のほうが厳しかったのではありません。
根拠がないまま厳しく書いてあった、というのが正確でした。
キャストで消さなかった理由
この赤線は、1行で消せます。
const items = data as Item[]
これを書けば、以降は3種類のどれかとして扱えます。書き心地も戻ります。
ただ、as は「必ず3種類のどれかだ」と主張する記述です。
誰も確かめていません。
check 制約を後から外したとしても、この行は何も言わなくなります。
前回の : Item と同じ場所に戻るだけでした。
なので Pick で必要なカラムだけ切り出して、type は string のまま受けました。
export type Item = Pick<
Database['public']['Tables']['items']['Row'],
'id' | 'type' | 'title' | 'description' | 'url' | 'sort_order'
>
いま弱いのを受け入れられるのは、弱いことが見えているからです。
副産物もありました。select() から取ってくるカラムを1つ減らすと、Pick の側にも赤線が出ます。
取ってきていないものを使っている、がコンパイル時に分かるようになりました。
弱いままにするなら、別の場所で守る必要があります
ここで止めておけない事情があります。
弱くなった型は、根拠がなかった場所が見えたという意味だけではありません。
それまで型が止めていたものを、誰も止めなくなったということでもあります。
"link" | "note" | "feed" と書いてあった間は、それ以外の文字列を入れようとすると
その場で tsc が止めていました。string になった今は、どんな文字列でも通ります。
止まるのは、実際にデータベースへ届いて check 制約に弾かれた瞬間だけです。
対処は2つあります。どちらを選ぶかは設計の判断。
- データベース側に制約を足して、生成型を強くする
- アプリ側で絞る型を別に定義して、入口で検証する
どちらもやらない、という選択肢は事実上ありません。
おわりに
型を生成すれば手書きより良くなる、と思って作業を始めました。
実際に見えたのは、自分が思い込みで書いていた制約のほうでした。
弱くなった1箇所は、劣化ではなく、もともと根拠がなかった場所です。
そこを型で押さえているつもりだった、というのが今回の収穫でした。
そして、この判断の答え合わせは同じ日の夕方に来ました。
外れた関門をまだ立て直していない状態で、
存在しない値が入りかけるところまで行っています。
次回はその話を書きます。
