[TypeScript]契約プログラミングによるAssertionの調査と整理
ProgramaticProgrammerのDbC(契約プログラミング:契約による設計)を触れてここらへんを実際のプロダクトでどう落とし込むかを整理
TSだと3.7ですでに機能として存在するので、これで型解決できそう
Nodeにassertライブラリがbuilt-inで入ってる(知らなかった)。
ただ、他の言語だとたいていBuildするとassertの機能は取り除かれるが、NodejsではProductionでも使われる。もし取り除きたいならunassertとかを使うみたい。
実際の書き方の例とか
ただ、以前にKent C Dodds がTSだとAssertionは本番にもデプロイしたほうが良くね、みたいなことをツイートしてたような記憶があるので、そこらへんをちょっと探してみる。ちな、理由としては、動的言語なので結局は本番でもきちんとチェックしたほうが良いみたいな理由だった気がする。
import assert from "assert";
const str = "THIS IS STRING" as string | number | undefined;
// str: string | number | undefined
assert(typeof str === "string" || typeof str === "number");
// str: string | number
assert(typeof str !== "string"); // => AssertionError
Node.jsのbuilt-inのassert の使い方を試してみた。
- 他にもいろいろ関数がある。REF: Assert | Node.js v17.7.2 Documentation
- ただ、そもそもbuilt-inよりも3rd partのライブラリのほうが機能的にリッチなのでプロダクションで使うならそっちのほうがよさそう
具体的に、どの3rd partyのライブラリが良いのか調べる。
ただ、以前にKent C Dodds がTSだとAssertionは本番にもデプロイしたほうが良くね、みたいなことをツイートしてたような記憶があるので、そこらへんをちょっと探してみる。ちな、理由としては、動的言語なので結局は本番でもきちんとチェックしたほうが良いみたいな理由だった気がする。
Don't use TypeScript generics that are just there to lie to you. Only through use of type guards and assertion functions (runtime checks) can you truly have end-to-end type checking across i/o boundaries.
thread内
Yeah, basically you can use zod to throw an error if the type is wrong and then your error boundary for the route will get rendered instead.
まとめるとこんな感じの意見みたい。
- i/o boundariesではgenericsとかで型解決しても、それが実態と異なることがある
- 唯一の確実なe2eの確認方法はguard/assetionによるruntimeでの確認のみとなる
- 具体的にはzodで型チェックしてerrorをthrowしてあげること
次は、zodについて調べてみる
zodの日本語の記事(日本語のほうが楽に読めるんでとりま英語記事は後回し)
- Zodで真のTypeScript firstを手にする
- Zodを試す
- Reactで使えるバリデーションライブラリを紹介! - bagelee(ベーグリー)
- 外からやってくる値から TypeScript の型を守るライブラリ・ツールまとめ - Mobile Factory Tech Blog
- TypeScriptのゾッとする話 ~ Zodの紹介 ~
読んで感じたこと、学んだこと
- この領域のパッケージは以前からそれなりにあるみたい。あまり自分が知覚してなかった。validator、runtime checkerみたいな文脈。FormのValidatorとしては知ってたけどio boundaryやそれ以外でも使うことは盲点だった。
- 2021年らへんから盛り上がってる?的な感じがある
- それぞれのパッケージについて一長一短がある感じがする
- とはいえ、zodがやはり良さそう
- 後発かつTS first。union/intersectionの両方を使えたり、実装から型を生成できたりする。
- ネックなのはまだ機能が少ない?ただ、これを書いてる時点ではすでにv3まで出てるのでそうでもないのかも。
ここらへん調べてて脳内がちょっと整理されたのでわかりやすくロジックの安全性をLv別にしてみた(個人の意見です)
- Lv0 nullチェックがない。
- Lv1 nullチェックしてる。ifがネストしてる。
- Lv2 guardでチェックして早期リターンしてる。
- Lv3 DbC(事前/事後/条件不変)をassertで確認してる。
Lvが高いほうが必ずベターとは限らないがプロジェクト全体で見たときにどのLvにまで到達してるかは指針として考えておいたほうが良いかも。
x is Type
/ asserts x is Type
TSにてisの使い方がちょっと混乱したので整理
/** x is Type は返り値boolean**/
function fn1(item: unknown): item is String {
return typeof item === "string"
}
/** asserts x is Type は返り値void かつassert error のときはthrowする**/
function fn2(item: unknown): asserts item is String {
if(typeof item === "string") return
throw new Error("invalid")
};
めちゃくちゃはまった点として、
- arrow function だと assertion functionの結果をpredictする機構が動かないときがある
- assertion function を実行する側にvoidをつけると同じくpredictが動かないときがある
const assertX = (maybeX: any): asserts maybeX is X => {
...
}
// maybeX: any;
void assertX(maybeX)
// => maybeX: any
// predictされない
ちゃんとバージョンと挙動を検証してないので、あとで時間があれば見てみる