$30 off During Our Annual Pro Sale. View Details »

品質とスピードを両立: TypeScriptの柔軟な型システムをバックエンドで活用する

kosui
March 26, 2024

品質とスピードを両立: TypeScriptの柔軟な型システムをバックエンドで活用する

2024/03/26
TypeScript 開発言語を統一 〜フロントからバックまで活用〜 Lunch LT
登壇資料

https://findy.connpass.com/event/312847/

kosui

March 26, 2024
Tweet

More Decks by kosui

Other Decks in Programming

Transcript

  1.  https://kosui.me ﵵ @kosui.me ﰏ @kosui_me kosui / 岩佐 幸翠

    日本の医療体験をしなやかにするために 医薬業界向けのサービスを多面的に展開 医療システムに要求される高い品質と 医療体験を本気で変えるためのスピードを両立させたい プロダクト開発チームと一緒に テックリードとして認証基盤・組織管理システムなどを開発  株式会社カケハシ  社内プラットフォームシステムの開発 2 2
  2. バックエンドとフロントエンドの共通点 リンター、フォーマッター、パッケージマネージャーなど Array 、 Promise 、 JSON など keyof などの型演算子や、

    Partial<T> などのユーティリティ型などの活用方法 言語を統一すれば、共通の知見や資産を活用できる エコシステムへの知見や設定 標準ライブラリへの知見 型の表現 3 3
  3. バックエンドとフロントエンドの相違点 フロントエンド: 使用性を重視 エラーを明快にフィードバックするべき 選択肢に無い値を選んだ場合のエラー文言は不要 バックエンド: 信頼性と機能性を重視 悪意あるユーザーや想定外の動作環境などを考慮し より厳密に検証するべき const

    FRUITS = [" りんご", " みかん", " ぶどう"] as const; if (FRUITS.includes(selected /* 納豆 */)) { throw new Error("invalid"); } ? 言語を統一すれば、設計や実装の技法も統一できる? ! そうとは限らない 例: バリデーションの場合 4 4
  4. 役割と品質特性から見たバックエンド 機能性 定義された I/F を満たし、正しくセキュアに処理できる 信頼性 安定して動作し、障害やエラーから復旧できる  会計・決済など正確性が重要な 処理は改ざん防止のため

    バックエンドで行う  プロダクトの性質に応じて 一貫性や整合性などを考慮して データを永続化する  全てのユーザー・顧客の 個人情報や認証情報など リスクのあるデータを扱う バックエンドで重視される品質特性 バックエンドの役割 正確性が重要な処理 データの永続化 非公開データの参照 7 7
  5.  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
  6. 構造的部分型の特性とバックエンドの不具合 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
  7. タグ付きユニオン - エンティティの区別 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
  8. 幽霊型 - リテラル型の区別 実行時には存在しないプロパティを追加する幽霊型は 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