Slide 1

Slide 1 text

Typia で 配信 JSON の安全性を構造的に担保する 株式会社RightTouch / oiki 1

Slide 2

Slide 2 text

自己紹介 oiki ( @kn_prg ) 大手 Web 系 → AI 系ベンチャー → 2022 年から RightTouch TypeScript を軸に、FE から SaaS の 0-1 立ち上げまで いまは プロダクトエンジニア / Hiring Manager 趣味はランニング 2

Slide 3

Slide 3 text

自己紹介 oiki ( @kn_prg ) 大手 Web 系 → AI 系ベンチャー → 2022 年から RightTouch TypeScript を軸に、FE から SaaS の 0-1 立ち上げまで いまは プロダクトエンジニア / Hiring Manager 趣味はランニング 3

Slide 4

Slide 4 text

RightTouch の紹介 カスタマーサポート領域 に向けた SaaS プロダクトを複数展開 運用するほど精度が高くなる AI オペレータ をコアとして、 AI コンタクトセンター を実現していく どのプロダクトもFull TypeScript 4

Slide 5

Slide 5 text

配信 JSON とは RightTouch のプロダクトで、顧客のサイトに配信している JSON ファイル 管理画面で設定 → 顧客サイト上に表示 3rd party っぽいスクリプトのあるページで Network タブを覗くと、 大抵こういう JSON が配信されている → 誰でも中身を見られる 5

Slide 6

Slide 6 text

目指すところ 目的 安全性: 配信 JSON に 不要な値が入らない こと 構造的: ヒューマンエラーで 容易に混入できない 仕組みであること 基本方針 安全性 → ブラックリストではなく ホワイトリスト 構造的 → 型の二重管理をなくす、変更に確実に気づける 6

Slide 7

Slide 7 text

当初は既存のライブラリを流用しようとしたが... 元々プロダクト内で使っていた JSON Schema ベースの validator を流用 する想定だったが、再帰などを含む複雑な型でうまく扱えず 下記のような バリデーション用の型を別途定義 していた type ActionInRequest = Pick & { // urlCondition が再帰的に定義されている関係で validator がうまく扱えない urlCondition: any; }; 7

Slide 8

Slide 8 text

じゃあ何をつかうか? 主な要件 型から直接バリデーションコードを生成できる 独自のスキーマなどで別途定義を書く必要なし TSの表現をそのまま活用できる 再帰・generics・union などを多分に含む型もそのまま扱える 8

Slide 9

Slide 9 text

じゃあ何をつかうか? 主な要件 型から直接バリデーションコードを生成できる 独自のスキーマなどで別途定義を書く必要なし TSの表現をそのまま活用できる 再帰・generics・union などを多分に含む型もそのまま扱える → ほかのプロダクトでの利用知見もあり、Typia を採用 9

Slide 10

Slide 10 text

Typia とは TS の型をそのまま 実行時バリデータに使えるライブラリ スキーマや builder を別途書く必要なし コンパイル時 に、型に対応する処理コードへ展開 実行時は素の関数呼び出し → 高速・軽量 validate / is / assert / assertPrune / clone … など多機能 10

Slide 11

Slide 11 text

Typia とは TS の型をそのまま 実行時バリデータに使えるライブラリ スキーマや builder を別途書く必要なし コンパイル時 に、型に対応する処理コードへ展開 実行時は素の関数呼び出し → 高速・軽量 validate / is / assert / assertPrune / clone … など多機能 今回の場合、createPrune() という関数を使えば解決できます(完) 11

Slide 12

Slide 12 text

Typia は何をしているのか 大まかな処理の流れ 1. コンパイル時にASTから typia.xxx(...) を検出 2. TypeCheckerで対象の型を取得 3. 取得した型を Metadata に変換 4. Metadata からvalidationコードを生成 サンプルの型 type Tree = { name: string; children: Tree[] }; 12

Slide 13

Slide 13 text

取得した Type から Metadata に変換 Metadata の構造 { atomics: ["string", ...] arrays: [{ value, recursive }] objects: [{ name, properties, recursive }] // ... tuples / aliases / sets nullable: boolean optional: boolean } union は 配列に複数入る だけ null / ? は独立フラグ 二度到達したら recursive: true Tree の Metadata { objects: [{ name: "Tree", recursive: true, properties: [ { key: "name", value: { atomics: ["string"] } }, { key: "children", value: { arrays: [{ value: /* Tree */, recursive: true }] } }, ], }], } 13

Slide 14

Slide 14 text

Metadataからのコード生成 Metadata { objects: [{ name: "Tree", recursive: true, properties: [ { key: "name", value: { atomics: ["string"] } }, { key: "children", value: { arrays: [{ value: /* Tree */, recursive: true }] } }, ], }] } 生成されたコード: typia.validate() (input) => { const errors = []; const $vt = (v, path) => { if (typeof v !== "object" || v === null) { errors.push({ path, expected: "Tree" }); return false; } let ok = true; if (typeof v.name !== "string") { errors.push({ path: `${path}.name`, expected: "string" }); ok = false; } if (!Array.isArray(v.children)) { errors.push({ path: `${path}.children`, expected: "Tree[]" }); ok = false; } else { v.children.forEach((c, i) => $vt(c, `${path}.children[${i}]`)); } return ok; }; return { success: $vt(input, "$input"), errors }; }; 14

Slide 15

Slide 15 text

実行方式 Typia の実行方式は2種類ある Transformation モード: tsc のカスタムトランスフォーマーとしてコンパイル時に展開 Generation モード: CLI (typia generate ) で事前にファイル生成 → 生成した関数を呼び出す 今回は Generation モードを採用 CI で差分検知することで、型を変更した際の配信 JSON への影響を認知可能 Transformation モードだと自分たちの環境では HMR の速度が大きく劣化 15

Slide 16

Slide 16 text

最終的にやったこと 1. 配信 JSON の型から Omit を排除し、Pick するように変更 2. typia generate で validation コードを事前に生成 i. validatorの再生成 = 配信 JSON への変更として検知 const pruneSettings = typia.misc.createPrune(); 3. JSON ビルドロジックにて pruneSettings(data) して不要な値を除去 16

Slide 17

Slide 17 text

最終的にやったこと 1. 配信 JSON の型から Omit を排除し、Pick するように変更 2. typia generate で validation コードを事前に生成 i. validatorの再生成 = 配信 JSON への変更として検知 const pruneSettings = typia.misc.createPrune(); 3. JSON ビルドロジックにて pruneSettings(data) して不要な値を除去 比較的シンプルにやりたいことを実現できてハッピー! 17

Slide 18

Slide 18 text

Full TypeScript で、 一緒に最高のプロダクトを作る仲間を募集中! 🔍 RightTouch エンジニアブログ で検索! 18