TypeScriptで型定義を信頼しすぎず「信頼境界線」を設置した話

24K Views

November 20, 23

スライド概要

JSConf JP 2023

2023年11月19日 13:30-14:00(30 min) Track C で発表した内容になります。

https://jsconf.jp/2023/talk/himeno-kosei-1/

profile-image

HireRooは、エンジニア採用のコーディング試験サービスです。🦘エンジニアの技術力を多角的かつ定量的に評価することで、候補者と企業のミスマッチを防ぎます。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

TypeScriptで型定義を信頼しすぎず 「信頼境界線」を設置した話 JSConf JP 2023 トラックC 13:30-14:00 @Himenon 1

2.

自己紹介 姫野 滉盛 @Himenon SNS ● ● GitHub: Himenon Twitter: himenoglyph 所属 ● 株式会社ハイヤールー エンジニアリング領域 ● ● Webフロントエンド(メイン) 開発支援ツールを作るのが趣味 代表的なOSS ● Himenon/openapi-typescript-code-generator

3.

プロローグ

4.

型安全 Type Safe

5.

Q. 型安全なら アプリケーションは安全か?

6.

const add = (a: number, b: number): number => a + b;

7.

const add = (a: number, b: number): number => a + b; add(1, 2) // = 3 add(1, “2”) Argument of type 'string' is not assignable to parameter of type 'number'

8.
[beta]
const fetchValue = (): Promise<number> => { … }
const add = (a: number, b: number): number => a + b;

const a = await fetchValue();
add(a, 2);

9.

ぱっと見ると警戒する

10.
[beta]
この関数がPromise<number>を
必ず返すと信じることができるか?

const fetchValue = (): Promise<number> => { … }
const add = (a: number, b: number): number => a + b;

const a = await fetchValue();
add(a, 2);

11.

型が示す値の信頼性はどこで手に入るのか? 本発表で探っていきましょう

12.

Agenda 1. ブラウザ上で動くアプリケーションの環境 2. 値の検証の頻度と場所 3. 型と値の信頼性を高める方法 4. まとめ

13.

01 ブラウザ上で動くアプリケーションの環境

14.

01 ブラウザ上で動くアプリケーションの環境 Webフロントエンドアプリケーションが動く環境 User Interaction User Interaction: ● ● Browser フォーム入力 クリックイベント Call API: ● ● ブラウザのAPI(LocalStorage etc … ) サードパーティーライブラリのAPI Server Response: ● ● App fetch response websocket response Call API: Browser Third Party API Server Response

15.

01 ブラウザ上で動くアプリケーションの環境 着目するポイント1 Form入力 入力されたフォームの信頼できるかどうか? (Optional)ユーザー入力に提供する値 <form> <input name="message" type="text" /> App Browser <select name="item"> <option value="A">A</option> <option value="B">B</option> </select> </form> ユーザーの入力によって返ってきた値

16.

01 ブラウザ上で動くアプリケーションの環境 着目するポイント2 クライアント・サーバーモデルのレスポンスの値 Request RequestはServerによって検証される ● リクエストはサーバー側で悪意が無いか検証する ○ = Validation 安全な値 Client Server → 信頼できる Response ● ● 大抵の場合そのままレスポンスを受け取って アプリケーション内部で利用する → 信頼できるかどうか? Responseは検証される...?

17.

01 ブラウザ上で動くアプリケーションの環境 着目するポイント3 APIから返ってくる値 例えばlocalStorageから取得した値: window.localStorage.setItem const value = window.localStorage.getItem("KEY"); App Browser window.localStorage.getItem

18.

もう少し抽象的に見る

19.

01 ブラウザ上で動くアプリケーションの環境 ここでの考え方 アプリケーションの内側と外側がある 外向きのデータはアプリケーション自身が信頼した値 内部(App) 外側 外側は規模や種類に違いは あってもこの値の信頼関係の 構造は変わらない 内向きのデータはアプリケーションがまだ信頼性できていない値

20.

01 ブラウザ上で動くアプリケーションの環境 Q. なぜ値の信頼性が重要なのか? A. ソフトウェアが壊れるリスクを減らすため

21.

01 ブラウザ上で動くアプリケーションの環境 ソフトウェアが壊れる? Retrospective and Technical Details on the recent Firefox Outage - February 2, 2022 https://hacks.mozilla.org/2022/02/retrospective-and-technical-details-on-the-recent-firefox-outage/ 原因は? Actual: “content-length: …” Expected: “Content-Length: …”

22.

01 ブラウザ上で動くアプリケーションの環境 型はあっているが期待値ではないケース 最初のadd関数の例 ● add(NaN, 1); // => NaN これは期待値ですか? → 同じnumber型でもadd関数の期待値ではない値もある。 期待値 = 提供したい機能が正しく動作するための値

23.

01 ブラウザ上で動くアプリケーションの環境 クライアントとバックエンドの互換性が消失 互換性が失われるケース1 Server v1 v2 リリース時 ● 互換性なし クライアントに対して互換性のない変更がサーバー側 に入った場合。逆もしかり。 Client v1 v2 対策ができていないと簡単に障害につながるようなエラーが 発生する。 ※長時間使うようなクライアントアプリケーションは すぐに次のバージョンに更新されるとは限らない

24.

01 ブラウザ上で動くアプリケーションの環境 クライアントとバックエンドの互換性が消失 互換性が失われるケース2 Local Storage v1 ローカルストレージのスキーマを変えたときに互換性がなくなり参 照エラーを起こす。 過去のデータをマイグレーションをするか、破棄する必要がある。 v2 互換性なし App v1 v2

25.

02 値の検証の頻度と場所

26.

02 値の検証の頻度と場所 値の検証をしなかったどうなるか?

27.

02 値の検証の頻度と場所 (いつか)バグが発生する

28.

02 値の検証の頻度と場所 + バグの発生場所と根本原因の場所がずれる

29.

02 値の検証の頻度と場所 バグの発生場所と根本原因の場所がずれるとは 値の安全性が1度壊れると、再検証されるまで型があっていても値は壊れたまま 型安全だが 期待値ではない API 1 API 2 型安全 API 3 API 4 値を利用しないので 壊れなかった API 5 API 6 信頼できない値として伝搬 開発者が 最初に確認する場所 エラー発生 API 7 API 8 呼び出されない

30.

では、壊れるのが怖いので どこでも値検証すると?

31.
[beta]
02 値の検証の頻度と場所
どこでも値検証するとどうなるか?
●
●

どんなコードにも値のチェックをするためのロジックが含まれている。
誰が値の検証の責務を担当しているのかわかりにくい → 変更しにくい
type Kind = "FRUIT" | "ANIMAL" | "HUMAN";
const method1 = (kind: Kind) => {
if (kind === "FRUIT" || kind === "ANIMAL" || kind === "HUMAN") {
// do something…
}
}
const method2 = (kind: Kind) => {
if (kind === "FRUIT" || kind === "ANIMAL" || kind === "HUMAN") {
// do something…
method1(kind);
}
}
method2(JSON.parse(window.localStorage.getItem("KIND")));

32.

02 値の検証の頻度と場所 値を過信し過ぎても❌ 値の検証をしすぎても❌ 値の検証をしなかった場合 ● ● 問題が発生する時、重大な障害を発生させることがある 根本原因とエラーの発生位置が遠いためトラブルシューティングと対応が遅れる 過度に値検証をした場合 ● ● 読みにくい どの関数にも検証ロジックを書くのか...?ときりがない

33.

02 値の検証の頻度と場所 ちょうどいいバランスを見つけるためには値を信頼するための境界線が必要 API 外側から内側 にくるデータ 値検証 API API API API 外側から内側 にくるデータ 値検証 API

34.

02 値の検証の頻度と場所 小規模なアプリケーション 想定 ● ユーザーが自ら復旧できるようなアプリケーション ○ CLIや開発者ツールなど 値の信頼境界線の具体例 ● 期待値ではない値が来た場合にExceptionを投げる

35.

02 値の検証の頻度と場所 大規模なアプリケーション(成長するアプリケーション) 想定 ● ユーザーが自ら復旧しないアプリケーション ● 複数人で開発・保守が必要なアプリケーション 値の信頼境界線の具体例 ● ライブラリ: zod / ajv/ yup / joi … or 自分たちで書く ○ ● ● → JSON.parseの結果やForm入力のバリデーションに有効 Schema駆動開発(protobuf / GraphQL / OpenAPI ) ○ → fetchのレスポンスの検証に有効 ○ 自動生成されるコードに値検証やBreaking Changeの検知が含まれていることも。 トラフィックのロードバランス or 強制アップデート ○ → 古いリソースにアクセスするユーザーに対して有効

36.

03 型と値の信頼度の両方を高める

37.

03 型と値の信頼度の両方を高める #1 値の信頼境界を設置する ● ● 何を信じるのか どこを検証すべきなのか を明確にする #2 関数の責務を明確にする ● 関数の責務が肥大化させない

38.

03 型と値の信頼度の両方を高める どこにどんなテストをするのか? 値が信頼できない場所 ● 正常系・異常系などのテスト項目を設ける ● 値の検証(バリデーション)が正しく動いているかが重要な確認項目になる

39.

03 型と値の信頼度の両方を高める どこにどんなテストをするのか? 値が信頼できる場所 ● 型定義に加えて正常系(期待している動作)を中心にテストする ● 期待していない値を入れてわざとテスト項目を難しくする必要はない

40.

03 型と値の信頼度の両方を高める ログの収集と信頼境界のメンテナンスを怠らない 信頼境界としての精度を上げ続ける ● エラーの発生場所とその理由の距離を常に観測し続けること 予期せぬエラー API 値検証 エラーの原因 API エラー API エラー API API

41.

04 まとめ

42.

04 まとめ 型安全と期待値 ● ● 期待値 = 提供したい機能が正しく動作するための値 型安全な実装に期待値が必ず入る保証はない アプリケーションの外側と内側 ● アプリケーションには外側と内側があり、外側から内側に向かうデータは100%期待値であると信頼できない 値の信頼境界・区間 ● ● 外側からやってきた値によって引き起こされたエラーの発生場所は根本原因と離れていることがある この距離を小さくするために、値検証を行う境界線を設置 型と値の信頼度を高めていく ● ● 信頼境界は異常系のテストを実施することも検討できるが、信頼区間内の実装で異常系は過剰になることがある 信頼境界自体の信頼度を高めるために、ログを収集し、エラーの発生場所と原因の場所を観測する