/note/tech

サーバサイドTypeScriptを選ぶ前に向き合ってほしいこと

要約:

■ 1. 記事の概要

  • 著者は医療機関向けサービスを展開する組織でテックリードを務め、認証基盤・ID基盤・ライセンス基盤・証明書基盤などミッションクリティカルな領域の開発・運用に4年間携わってきた
  • サーバサイドTypeScriptに苦しみながら向き合い続けた経験から得た洞察を共有することが目的

■ 2. プログラミング言語への関心を払い続ける理由

  • Coding Agentの台頭により「どの言語を選んでも目的を達成できる」という空気が広がっているが、それは誤りである
  • 言語・実行環境・非同期ランタイム・エコシステムの特性は、アプリケーションコードを書くだけでは解決できない
    • CPUバウンドな処理が苦手な言語で複雑な計算をさせても性能は出ない
    • VMの起動が遅い言語でサーバレス構成を選んでもスケールしない
  • ビジネスやプロダクトの機能要求・非機能要求によってシステムに求められる能力は変わり、各言語には得意・不得意がある
  • 特にミッションクリティカルな領域(金融・医療・製造業・物流など)においては、思考停止した態度は許されない

■ 3. TypeScriptを選ぶ目的の明確化

  • コード資産の共有:
    • フロントエンドとバックエンドで型やスキーマを共有したいというのが最も多い理由だが、代替手段は多数存在する
      • スキーマ共有だけならOpenAPIなど複数の手段がある
      • 複雑なロジックの共有にはWASMという選択肢もある
    • フロントエンドとバックエンドで本当に同じコードを動かす必要があるケースは限られている
    • 例外として、オフラインでも稼働する医療システムの診療報酬計算ロジックのように、ネットワーク切断時にもクライアントで同一計算を再現しなければならない場合には、コード共有の明確な理由がある
    • 他に選択肢がある中でTypeScriptを選ぶならば、その理由を感覚的なものから体系立てられた言葉にする必要がある
  • 人材採用における母集団の広さ:
    • TypeScriptは様々な領域で利用されており、経験者の絶対数は増えているが「TypeScriptの経験者」が即戦力になるとは限らない
    • 領域によって払うべき関心や求められる設計は大きく異なる
      • フロントエンド: 使用性が重要であり、例外をぶん投げてエラー画面を提示する方が良いケースが多い
      • バックエンド: 機能完全性・可用性が重要であり、エラーの種類の判別・伝搬、トランザクション管理、リソース解放などの関心が特有
    • 同じ領域でも、デコレータとクラスを活用したオブジェクト指向、関数型ドメインモデリング、プロトタイプベースなど、チームによって設計が大きく異なる
    • サーバサイドTypeScriptを採用する際は、チームに必要とする人物像を事前に明確化すべきである

■ 4. 言語の特性と向き合う

  • TypeScriptには3つの固有の特性があり、それぞれが落とし穴を生む
  • 構造的部分型:
    • 型の互換性はクラス名ではなく構造(プロパティの構成)で決まる
    • Userを受け取る関数に同じ構造を持つProductを渡してもエラーにならない
    • テストダブルの差し替えが容易になる利点がある一方、意図しない型の混同を許してしまうリスクがある
  • 型消去:
    • TypeScriptの型情報はトランスパイル時にすべて削除され、実行時は単なるJavaScriptになる
    • 構造的部分型によってRectangle型として受け入れられたオブジェクトに対してinstanceof Rectangleがfalseを返すことがある
    • 型検査時のメンタルモデルと実行時の振る舞いにずれが生じる
  • プロトタイプベースとclassの限界:
    • thisの指す先は呼び出し方で動的に決まり、メソッドを変数に代入して呼び出した瞬間にthisがundefinedになりTypeErrorで落ちる問題を型検査は検出しない
    • ECMAScriptのclassの表現力は他言語に比べて限定的であり、#privateとTypeScriptのprivateは別物
    • useDefineForClassFieldsフラグによって同じコードでもtsconfigの設定で振る舞いが変わる(TypeScriptがES2015より3年先にclassを実装し、後からECMAScript仕様と統合した歴史的産物)
    • 型の表現力が高いことは自由度の高さも意味し、行き過ぎた抽象化や難解なメタプログラミングを誘発する原因にもなりえる
  • 著者の乗り越え方(全てを値で表現する):
    • classを使わず、プレーンなオブジェクトで全情報を表現するアプローチを採用
    • 構造的部分型を逆手に取り、型検査時と実行時の挙動の乖離をほぼ排除できる
    • Branded Typeで構造が同じ型を区別し、Discriminated Unionで種別を判別し、thisを持たない関数で振る舞いを表現する

■ 5. 実行環境の特性と向き合う

  • Node.jsの特性:
    • I/Oバウンドなタスク(データベースや外部サービスとの通信が支配的なワークロード)を得意とする
    • CPUバウンドなタスクはシングルスレッド構造からして苦手であり、1つのリクエストのCPU処理中に他の全リクエストが待たされる
  • Worker Threadsによるマルチスレッド化の反論に対して:
    • スレッドプールの管理やスレッド間メッセージングなど複雑性が増すため、その複雑性を受け入れる必要があるかを検討すべきである
    • その要求に自然にフィットするランタイムや言語が他にあるはずである
  • 著者の実例(医療システムの認証基盤):
    • パスワードのハッシュ化がCPUバウンドな処理であり、セキュリティ上の妥協ができない
    • ハッシュ化ライブラリによってlibuvを活用したマルチスレッド化済みのものと、Worker Threadsの利用を利用者に委ねるものがあり、対応が変わる
  • サーバレス環境(AWS Lambda・Cloudflare Workersなど)では、1リクエストに1実行環境が割り当てられるため、CPUバウンド処理の問題は大きく緩和される
  • 言語だけでなく実行環境の得意・不得意がビジネス・プロダクトの要求にマッチしているかを検討する必要がある

■ 6. 結論

  • 昨今、サーバサイドTypeScriptをやめる組織もあれば、今から移行しようとする組織もある
  • 技術の移行判断においては、チーム内外・組織内外への説明責任が求められる
    • 本当にその移行が必要なのか
    • 言語をスケープゴートにして本質的なプロダクト品質やチーム体制の課題から逃げていないか
    • 目の前にある技術とどこまで向き合ったのか
  • 安易に選ぶのでも安易に辞めるのでもなく、ビジネスとプロダクトと、何よりも技術ときちんと向き合い続けることが重要