Share
Embed
品質とスピードを両立: TypeScriptの柔軟な型システムをバックエンドで活用する
2024/03/26
TypeScript 開発言語を統一 〜フロントからバックまで活用〜 Lunch LT
登壇資料
https://findy.connpass.com/event/312847/
kosui
March 26, 2024
Transcript
15 slides
-
https://kosui.me ﵵ @kosui.me ﰏ @kosui_me kosui / 岩佐 幸翠
日本の医療体験をしなやかにするために 医薬業界向けのサービスを多面的に展開 医療システムに要求される高い品質と 医療体験を本気で変えるためのスピードを両立させたい プロダクト開発チームと一緒に テックリードとして認証基盤・組織管理システムなどを開発 株式会社カケハシ 社内プラットフォームシステムの開発 2 2 -
バックエンドとフロントエンドの共通点 リンター、フォーマッター、パッケージマネージャーなど Array 、 Promise 、 JSON など keyof などの型演算子や、
Partial<T> などのユーティリティ型などの活用方法 言語を統一すれば、共通の知見や資産を活用できる エコシステムへの知見や設定 標準ライブラリへの知見 型の表現 3 3 -
バックエンドとフロントエンドの相違点 フロントエンド: 使用性を重視 エラーを明快にフィードバックするべき 選択肢に無い値を選んだ場合のエラー文言は不要 バックエンド: 信頼性と機能性を重視 悪意あるユーザーや想定外の動作環境などを考慮し より厳密に検証するべき const
FRUITS = [" りんご", " みかん", " ぶどう"] as const; if (FRUITS.includes(selected /* 納豆 */)) { throw new Error("invalid"); } ? 言語を統一すれば、設計や実装の技法も統一できる? ! そうとは限らない 例: バリデーションの場合 4 4 -
この発表のねらい バックエンドのような信頼性・機能性が重要なシステムを TypeScript で開発するためには? TypeScript の型システムとバックエンドの相性の良い部分・悪い部分を理解する TypeScript の型システムの柔軟性を活かしたデザインパターンを使いこなす おことわり
私の発表ではバックエンドにフォーカスしますが 「フロントエンドとの連携」などについては、この後の発表をご期待下さい! ? 問題 解決策 方針 5 5 -
役割と品質特性から見たバックエンド 機能性 定義された I/F を満たし、正しくセキュアに処理できる 信頼性 安定して動作し、障害やエラーから復旧できる 会計・決済など正確性が重要な 処理は改ざん防止のため
バックエンドで行う プロダクトの性質に応じて 一貫性や整合性などを考慮して データを永続化する 全てのユーザー・顧客の 個人情報や認証情報など リスクのあるデータを扱う バックエンドで重視される品質特性 バックエンドの役割 正確性が重要な処理 データの永続化 非公開データの参照 7 7 -
TypeScript が採用している構造的部分型 型の名前で互換性を判定する 以下は Java の例 // Java record
Animal( String name ) {} record Human( String name, int age ) {} Animal animal = new Human("kosui", 29); // ^^^^^^^^^^^^^^^^^^^^^^ // Incompatible types... オブジェクトの構造で互換性を判定する 記述量を抑えつつ型システムの恩恵を得られる // TypeScript type Animal = { name: string; }; const human = { name: "kosui", age: 29, }; const animal: Animal = human; // `human` がAnimal を継承しなくてもビルドが通る 名前的部分型 ( 公称的部分型) 構造的部分型 9 9 -
構造的部分型の利点 継承関係 ( 祖先・子孫) を認知する負荷を低減できる 記述量を抑えつつ型システムの恩恵を得られる 同じ構造を持つオブジェクトについて型の詰め替えを省略できる 例) データベースから取得したデータからエンティティへの詰替え テスト用のダミーやモックを簡便に表現できる
// animal.ts type Animal = { name: string; }; interface AnimalRepository { find(id: string): Promise<Animal>; } // animal.test.ts const dummyRepo = { find: (id: string) => Promise.resolve({ id, name: "foobar" }), }; 10 10 -
構造的部分型の特性とバックエンドの不具合 type User = { userId: string; }; type Role
= { userId: string; role: string; }; const UserRepository = { delete: (user: User) => Promise<void>, }; const deleteRole = async (role: Role) => { // Role と間違えてUser を削除している await UserRepository.delete(role); }; 構造的部分型では構造さえ同じなら何でも通す しばしば思わぬ結果をもたらす 例) リポジトリに誤ったオブジェクトを渡しても ビルドと実行が成功してしまう 永続化処理は不具合があると復旧が難しい 例) オブジェクトに機密データのプロパティが 含まれていることに気付けない データ更新時の不具合 機密データの露出 11 11 -
タグ付きユニオン - エンティティの区別 type User = { kind: "User"; userId:
string; }; type Role = { kind: "Role"; userId: string; role: string; }; type Entity = User | Role; const UserRepository = { delete: (user: User) => Promise.resolve(), }; const deleteRole = async (role: Role) => { await UserRepository.delete(role); // ^^^^ // Argument of type 'Role' is not ... }; リテラル型の値から型を絞り込むことができる kind プロパティでエンティティを区別する User と Role を 取り違えて削除してしまうのを防げる タグ付きユニオン 利用例: エンティティの区別 13 13 -
幽霊型 - リテラル型の区別 実行時には存在しないプロパティを追加する幽霊型は string 型や number 型などのリテラル型を区別するのに便利 type Newtype<T,
Kind extends string> = T & { [key in `__${Kind}`]: never; }; 異なるエンティティの ID の代入を防げる type PostId = Newtype<string, "PostId">; type UserId = Newtype<string, "UserId">; const userId: UserId = "aaa" as UserId; const postId: PostId = userId; // ^^^^^^ // Type 'UserId' is not assignable ... 検証済みの値と生の値を区別できる type Username = Newtype<string, "Username">; const Username = { parse: (raw: string): Username | undefined => raw.length > 0 && raw.length < 16 && regexp.test(raw) ? (raw as Username) : undefined, } as const; const username = Username.parse("abc"); 利用例: ID ・コードの区別 利用例: バリデーション済みかを示す 14 14