KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

なぜバックエンドTypeScriptか?技術選定背景と実践例を紹介します

カケハシの医薬品発注管理最適化領域の新規事業の開発を担当している木村です。今回は新しいサービスを構築する上で行った技術選定と実践方法の話をします。
技術選定に関しては、インフラ関連やライブラリなど選定した技術は多岐にわたるのですが、その中でも「なぜバックエンドでTypeScriptを導入したか」を中心にお話します。2つのチームでの技術選定に関わり、どちらもTypeScriptを導入するに至りました。2022/03時点では社内の5つのサービスでバックエンドTypeScriptが採用されていることを観測しています。
実践方法に関しては、技術選定の過程で明らかになったシステム特性に対するアプローチを紹介します。

全社的な技術選定方法

カケハシではビジネスドメインで開発チームを分割し、開発チームが自走化できるように組織がデザインされています。技術選定についても開発チームに裁量があります。
技術選定は、ビジネスドメインにおける技術の有用性や、現場メンバーのスキルセットや志向性などを考慮した上で決定します。
もちろん無策に自由に技術を選んでいいわけではなく、会社で培ってきた技術観点をまとめたガイドラインがあり判断軸が設けられています。特別な理由がない限りは社内での知見の多く、運用がしやすい技術を選定します。
もし社内でシェアが低い技術や導入実績のないXaaSを導入したい場合は、起案者が導入起案やアーキテクチャ記述を用意し社内レビューの依頼をします。社内レビューは、CTOやEMに承認をもらうような権威的なものではなく、全社的に呼びかけてレビューを通して建設的な議論を行うために実施されます。

過去の社内の言語選定状況

社内におけるバックエンドのプログラミング言語は大半がPythonが採用されていますが、新しく開発するドメイン領域ではTypeScriptが採用されるケースも増えてきました。私が入社した2021年5月のタイミングでは認証マイクロサービスの1箇所でTypeScriptが既に選定されていました。強い静的型付けの言語で開発できる利点があり、bundle sizeを小さくしやすく、ウェブ開発に必要な周辺エコシステムが揃えやすくなったことが導入のきっかけだったそうです。

※ なお、フロントエンドはTypeScriptが採用されています。

医薬品発注管理最適化領域の事例

医薬品発注管理最適化領域における新規事業のプログラミング言語は結果的にバックエンドもフロントエンドもTypeScriptになりましたが、最初から言語選定をしていたわけではありません。むしろ、業務領域が近いAI在庫管理のバックエンドはPythonで構築されていたため、Pythonが最有力候補でした。

コンテキストから境界と求められるシステム特性を洗い出す

技術選定に関わらず、新しいサービスを象る時にはコンテキストマップ(※1)を作成することが多いです。コンテキストマップを記述することで問題領域とコンポーネント、それらの関係性が可視化されます。
ただ、プロジェクト初期のコンテキストマップはシステムを概観するためのものであり、ドメインモデリングを進める過程でコンテキストマップはアップデートされます。ドメイン知識が深くない(境界の揺らぎがある)状態で初期のコンテキストマップを信じて早期にマイクロサービス化するのは英断になる場合もありますが、大抵の場合は早期の最適化になりがちです。
早期の最適化は避けつつ、ドメインモデリングを進めていった結果、既存のサービス(Python)に組み込む選択をしたコンテキストと明確に分離した方がリスクよりも利点につながるコンテキストの2種類があることに気が付きました。
明確に分離した方がいいコンテキストにおけるシステム特性として、堅牢性(Robustness)、正確性(accuracy)、試験容易性(testability)が重要で、スケーラビリティ(Scalability)はそこまで重要ではありませんでした。どの言語でも実装方法次第では上記特性を賄うことはできるかもしれませんが、静的型付け言語に軍配があがります。また、派生してImmutableプログラミング、コレクションの使いやすさ・合成可能性、エラーハンドリングといった観点も個人的には含めていました。「TypeScriptの魅力」と「実践」にて後述しますが、TypeScriptは十分にこれらの要求を満たすことができます。

※1 コンテキストマップについては、コンテキストマップの目的再考と運用ヒントという私の記事があるので参考にしてもらえると幸いです。

人員計画から技術を選択する

社内の知見と人員に関してはPythonに軍配があがっていましたが、その次はTypeScriptでした。TypeScriptは、フロントエンドの人員もアサインがしやすいメリットがあります。
なお、所違えばScalaやGoもアリだと思いますが、社内の知見と人員を加味すると難しく、社外のメンバーを採用したり社内で普及させるほどの積極的な選定理由はありませんでした。もちろん既存のスキルセットだけで選択すると先進技術のキャッチアップが遅れ、結果的に市場優位な状況を作れないリスクがありますが、TypeScriptのOSSは活発であり今後の発展の兆しがあるため大して気にする観点ではありませんでした。
当初確認していたState of JS 2020の利用率と満足度の高いTypeScriptであれば採用に関しても一定の期待ができました。

価値提供のリードタイムとロジックの可搬性

イテレーティブに価値を提供する際にフロー効率重視のプロダクト開発がFitする場合があります。フロー効率性は、価値提供のリードタイムを短くするための効率性のことであり、それと対比関係にある、リソース効率性はリソースの稼働率を高くすることを指します。
リソース効率性の例としては、職能別(ex. フロントエンドとバックエンド)に分業してプロセスの稼働率をあげる開発が挙げられます。
これら効率性は例外なくどちらを選べばいいという話ではなく、プロジェクト状況を踏まえてトレードオフを選択することが重要です。
とはいえ、本来的にフロー効率的に進めたくても、人員の技術スタックが異なることが要因してフロー効率的に進められず、現実解としてリソース効率的になるケースはよくあります。この要因は、フロントエンドもバックエンドも同じ(もしくは、類似の指向性をもった)言語にすることで多少緩和できる場合があります。
また、サービスの骨格が定まっていない立ち上げ期では特に、ロジックの所有先がフロントエンドなのかバックエンドなのか判断がつかない場合があります。(ex. ドメインモデルやバリデーション、レンダリングなど)。この場合は、言語を統一していればロジックの可搬性が担保でき、システム境界周りの冗長性の解決ができます。
これらは、Universal JavaScriptの考え方がとてもFitすると考えています。
なお、私とほぼ同時期に入社したメンバーが別の新規サービスを立ち上げる際にもUniversal JavaScriptの思想でフロントエンドとバックエンドだけでなく、インフラ(AWS CDK)を全てモノレポでTypeScriptに統一しコンテキストスイッチを最小にする工夫がなされていました。ドメインエンティティの型情報をフロントエンドとバックエンドで共有することでシステム境界を安全かつ効率的に開発できる狙いがあったそうです。

Serverless環境を加味する

既存のチームでは、特別な理由がない限りデリバリの高速化と運用原価の観点でServerlessな構成が主な選択肢となっており、特にLambdaなどのFaaSを使う事例が大半でした。(ちなみに医薬品発注管理最適化領域の新規事業では最終的にECS/Fargate環境を用意しました。)
The State of Serverless2020 を見ると圧倒的にPythonとNode.jsのシェアが高いことが分かります。
私は7年間ほどScalaでバックエンドとデータパイプラインの開発をした経験があり、求めているシステム特性をカバーできることを知っていましたが、例えばLamdbaで実行する際には、メモリ容量が大きくなりがちな点と暖機運転や、Native Image化などのアーキテクチャ上の検討をしなくてはならず、今回の要件にFitしませんでした。

TypeScriptの魅力

型システム

公式ドキュメントのTypeScript for Java/C# Programmersでは、JavaとC#との型システムの違いを紹介しています。
JavaとC#の型は公称型(Nominal Typing)で、TypeScriptの型は構造的部分型(Structural Subtyping)で構築されています。詳細の説明は公式ドキュメントを見てもらうのがよいかと思いますが、公称型は実行時の型とコンパイル時の型が1:1に紐づく特性がありますが、TypeScriptの構造的部分型は値の集合であると見立てられます。集合と見なすことで型から型を生成することができ、同じ形状をもった型定義をプログラム上で使い回せる柔軟性があり、型定義の冗長的宣言を解消してくれます。具体的にはTypeScript: Documentation - Creating Types from Types を参照してください。
とはいえ、形状が同じでもコンパイルエラーにしたいユースケースは堅牢性が求められる場面ではよくありますが、TypeScriptは公称型として扱う方法(※3)にも開かれていますのでそのユースケースに対応できます。

※3 公称型の実現方法は複数あり、Nominal typing techniques in TypeScript で紹介されています。

Immutableに扱える言語機構

TypeScriptのプリミティブ型(boolean, number, bigint, string, symbol,null, undefined)はプロパティを持たず不変です。
オブジェクトを不変に扱うためのReadonly、コレクションを不変に扱うためのReadonlyArray, ReadonlyMap, ReadonlySetが標準の型として用意されています。これらはネストしている配列やオブジェクトの不変性は担保しませんが、constアサーション(as const)を利用すると再帰的に不変になります。再帰的不変性をアサーションでなくて型として表現したい場合は、標準にはありませんが、type-festts-essentials といった型定義のユーティリティライブラリで補完することが可能です。これらライブラリは型から型を生成できる型システムの恩恵を受けており、標準との互換性があります。

コレクションAPI

TypeScriptはJavaScriptのsuper setなので、コレクションAPIはJavaScriptの機構に相当します。JavaScriptでのArrayオブジェクトにはmap/filter/reduceなどの様々な関数が用意されています。また、Arrayの他に、Map、WeakMap、Set、WeakSetの組み込みオブジェクトが存在します。
しかし、使える関数はArrayと比べると限定的ですし、Scalaなどの言語と比べてコレクションAPIと型は少なく、単方向リストもなくコレクションのパターンマッチの機構もありません。
コレクションAPIに関しては、immutable.jsfp-ts といったライブラリを活用することで表現力を拡充できます。

実践

全体のサービスにおいての実践ではなく、一部のサービス開発における個別の話です。

Webフレームワークとクエリービルダー

WebアプリケーションのWebアダプタとDBアダプタなどの有用なフレームワークやライブラリが存在しているかが重要になります。
フレームワークやライブラリは栄枯盛衰はあると思いますが、現時点ではWebアダプタはNestJSを採用し、DBアダプタはPrismaを採用しています。
NestJsに関しては、なぜNestJSを導入したか という記事を別途投稿していますので、参考にしてみてください。
Prismaは、型安全にSQLを理解している方に直感的なクエリービルダーDSLを提供していて、マイグレーションツールとしても使えます。
これらの存在により大規模なWebアプリケーションを開発ができる十分な機構があると判断しています。

fp-tsの導入

堅牢性(Robustness)、正確性(accuracy)、試験容易性(testability)をより強化するために、一部モジュールで関数型プログラミングのライブラリであるfp-tsを導入しています。

実現したかったことと解決策

1.エラーを型で表現し、合成容易な状態にしたい

2. エンティティの同一性を表現し、オブジェクトないしはコレクションを安全に扱いたい

3. 一部の型定義で公称型(nominal typing)を利用し、誤ったドメインロジックを早期発見できる仕組みが欲しい

気をつけている点と対策

気をつけている点

  1. 関数型プログラミングの前提となる深い知見がなければ実装が困難であること
  2. プログラム全域に対し純粋関数型の指向性で実装すること

対策

型制約を強く持たせたいドメインモデルにだけnewtype-tsを導入し、関数のシグニチャ自体は標準の型で表現しています。シグニチャ内部のドメインロジックにおいても表現の都合でfp-tsの関数を使うを利用する場合がありますが、関数内部は関数型っぽく書かなくてもシグニチャを満たす実装を書いていればよく、単体テストを書いて後からリファクタリングすればいいという割り切りをしています。これにより関数型プログラミングの深い知見がなくても開発可能な状態を作り、範囲を局所化しています。シグニチャ自体はドメインモデリングの過程で暫定的骨格ができるため、シグニチャの型を満たすコードを書いていくスタイルで開発しています。

以下は、ドメインモデルの制約を型で表現した実装サンプルです。

型安全でないJavaScript APIへの対処

TypeScriptでanyを使うと型の安全性が損なわれます。
自前実装でのanyの防止はeslintのno-explicit-anyオプションにて弾いています。 JSON.parseなど、組み込みの型定義がanyになっている場合は、better-typescript-lib の安全な型定義内容を活用しています。

まとめ

今回は、TypeScriptを技術選定した経緯と実践方法を紹介しました。
医薬品発注管理最適化領域の新規事業では現在開発している以外にも多くの技術選定やアーキテクチャ設計を行う必要がありますし、実践方法についても価値提供とチーム開発の視点で柔軟にアップデートしていくことが求められます。
こういった活動や設計、技術に興味があるエンジニアの方はぜひ仲間になっていただきたいと思っております。

※ 以下2つの求人にて今回話した内容を行っています。