Open9

[TypeScript]契約プログラミングによるAssertionの調査と整理

snamiki1212snamiki1212

ProgramaticProgrammerのDbC(契約プログラミング:契約による設計)を触れてここらへんを実際のプロダクトでどう落とし込むかを整理

snamiki1212snamiki1212

Nodeにassertライブラリがbuilt-inで入ってる(知らなかった)。
ただ、他の言語だとたいていBuildするとassertの機能は取り除かれるが、NodejsではProductionでも使われる。もし取り除きたいならunassertとかを使うみたい。


実際の書き方の例とか

ただ、以前にKent C Dodds がTSだとAssertionは本番にもデプロイしたほうが良くね、みたいなことをツイートしてたような記憶があるので、そこらへんをちょっと探してみる。ちな、理由としては、動的言語なので結局は本番でもきちんとチェックしたほうが良いみたいな理由だった気がする。

snamiki1212snamiki1212
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のライブラリが良いのか調べる。

snamiki1212snamiki1212

ただ、以前にKent C Dodds がTSだとAssertionは本番にもデプロイしたほうが良くね、みたいなことをツイートしてたような記憶があるので、そこらへんをちょっと探してみる。ちな、理由としては、動的言語なので結局は本番でもきちんとチェックしたほうが良いみたいな理由だった気がする。

https://twitter.com/kentcdodds/status/1490068789588725761

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について調べてみる

snamiki1212snamiki1212

zodの日本語の記事(日本語のほうが楽に読めるんでとりま英語記事は後回し)


読んで感じたこと、学んだこと

  • この領域のパッケージは以前からそれなりにあるみたい。あまり自分が知覚してなかった。validator、runtime checkerみたいな文脈。FormのValidatorとしては知ってたけどio boundaryやそれ以外でも使うことは盲点だった。
  • 2021年らへんから盛り上がってる?的な感じがある
  • それぞれのパッケージについて一長一短がある感じがする
  • とはいえ、zodがやはり良さそう
    • 後発かつTS first。union/intersectionの両方を使えたり、実装から型を生成できたりする。
    • ネックなのはまだ機能が少ない?ただ、これを書いてる時点ではすでにv3まで出てるのでそうでもないのかも。
snamiki1212snamiki1212

ここらへん調べてて脳内がちょっと整理されたのでわかりやすくロジックの安全性をLv別にしてみた(個人の意見です)

  • Lv0 nullチェックがない。
  • Lv1 nullチェックしてる。ifがネストしてる。
  • Lv2 guardでチェックして早期リターンしてる。
  • Lv3 DbC(事前/事後/条件不変)をassertで確認してる。

Lvが高いほうが必ずベターとは限らないがプロジェクト全体で見たときにどのLvにまで到達してるかは指針として考えておいたほうが良いかも。

snamiki1212snamiki1212

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")
};

ref: TypeScript: Documentation - TypeScript 3.7

snamiki1212snamiki1212

めちゃくちゃはまった点として、

  • 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されない

ちゃんとバージョンと挙動を検証してないので、あとで時間があれば見てみる