🗒️

TSkaigi kansai 参加レポート

2024/11/21に公開

初めに

2024/11/16に開催されたTSKaigi kansaiに参加してきました。(関東民なので流石にオンラインでの参加です)
ここでは、聴講したセッションやLTの概要や感想などを備忘録的にまとめました。

https://kansai.tskaigi.org/

セッション・LT

TypeScript Graph でコードレビューの心理的障壁を乗り越える

コードレビューをする上で感じる心理的障壁の理由を列挙して、中でもコード理解の壁にフォーカスしてそれらを解消するソリューションとしてTypeScript Grapthが紹介されました。
https://github.com/ysk8hori/typescript-graph
コードの理解の壁は、PRにおいてコードの変更差分からレビューを行う際に、一部の情報しか与えられていないことで、該当コードを理解するために、周辺のコードも含めて読むという高いコストを払わないといかない壁のことを指しています。
周辺のコードを読む際に、別のファイルやメソッドに依存しているときに、そのコードを探しに行く手間が発生します。(その際変更差分がないとエディタとかで探す手間が増える)
TypeScript Graphはファイル同士の依存関係を可視化するCLIツールで、手間をかけずに構造を理解することができるというものです。
発表ではpmndrs/jotaistorybookの依存関係を例に紹介されていました。
その中で循環参照になっている部分や、参照するはずなのにしていない部分などが説明されており、視覚化されていることで見やすく、中の実装を見なくても問題を発見できる点が魅力的でした。

先述したツールはCLIツールでPR上で動かせるようにGithub Actionsとして公開されていました。
https://github.com/ysk8hori/delta-typescript-graph-action

実際に業務で使用した事例も紹介されていました。

  • ある画面に表示項目を追加した
    • テストファイルが変更されていたが、テスト対象のファイル(依存元)が変更されていないことがグラフからわかった
  • 新規コンポーネントを追加した
    • 作成したコンポーネントがまだどこからも依存していないことがわかった
  • 依存先の多いコンポーネントをいじった
    • 依存先が多いためグラフがデカくなった
    • 変更箇所の責任範囲が大きいとこがわかる
    • ひとつのPRで多くのことをやろうとしていると考えられる

グラフが大きくなることは悪ではないが、大きくなる理由が適切かどうかは考える必要があるとのことでした。ただし、発表者の方の経験上、綺麗なグラフがいい構造ってわけでもないとのことでした。

ジョインしたてのメンバーがレビューするときに心理的負担を軽減できそうです。
ツールが個人で開発されたものなので社内で導入できるかは微妙なところ...

型付き API リクエストを実現するいくつかの手法とその選択

https://speakerdeck.com/euxn23/typed-api-request
このセッションではAPI側の型とFE側の型を一致させ型安全にする方法についてOpenAPIをキーワードとして解説されていました。
まず、APIと型の安全性を確保する手法として、コードファーストかそうでないかの二つに大別されます。
コードファーストではない手法として、APIの仕様書(OpenAPIベースではない)を書くことや、結合テストを増やすことが挙げられました。これは仕様書と実装が乖離したり、結合テストでテストケースが増えたりする課題があります。
コードファーストの手法では主に言語依存の手法と非依存の手法について紹介されていました。言語依存の場合はmonorepoを前提として、型定義の共有、Zodスキーマの共有、フレームワーク機能の利用が手法として挙げられていました。ただし、この方法はAPIとFEが同じ言語であることが前提であるため、片方を別の言語にすると安全性が失われるデメリットがある。
言語非依存の手法としてOpenAPIがキーワードとなっていました。OpenAPIと実装を繋ぐ手法として以下が挙げられました。

  • サーバーコードからOpenAPIを生成
  • OpenAPIからAPIクライアントを生成
  • OpenAPIからサーバーコードを生成

上記の手法は二つアプローチに分けることができます。どちらもメリットデメリットがあり。ケースによって使い分けることで効果を発揮します。

  • OpenAPIからコードを生成(特にクライアントコード)
    • 実装が変わったら型システムで検知できる
    • エンドポイントのパスやパラメータも補完・チェックできる
    • OpenAPIの実装から生成していない場合実装との乖離が起こる
  • コードからOpenAPIを生成
    • 仕様と実装が一致する
    • OpenAPIを手書きしなくていい
    • 実装を変更しないと仕様も変更されない
    • 実装ベースであるため、OpenAPIを中心に議論しずらい

まとめとして以下のように挙げられていました。

  • monorepoを前提としたサーバーとクライアントのTSコード共有は楽だが結合レイヤーではリスクがある
  • 結合レイヤーでは言語非依存のOpenAPIを用いるのが良いのではないか
  • OpenAPIで実装と仕様を近づけることでコード面の安全性と開発効率を高められることが期待できる。

OpenAPIを先に定義して実装に反映させるか、実装からOpenAPIの仕様書を吐き出すかは迷いどころですね。BFFなんかはAPI側の実装ありきな部分があるので、実装から仕様書吐かせる方が恩恵は受けられそうですね。

TypeScriptの名前空間を活用したUIコンポーネントの設計と型安全性の追求

https://sakupi01.github.io/slides/ja/2024_tskaigi_kansai/
TSXでUIコンポーネントを作る際に、Propsとコンポーネントの名前を分けて実装し、そのPropsを別のコンポーネントで使うとなると型を結合して別の名前にすることでどんどんProps名が冗長なることが起きます。
命名が冗長になるのは以下の理由が挙げられました。

  • 命名によってデータの性質構造を表現するため
  • 型が値に関するものであると表現したい
    • コンポーネント名+Propsみたいな命名にする
  • 上記を名前の重複がないように実装したいため
    • TSの型エイリアスは同一の名前はユニークである必要があるため

命名によっていろんな表現を詰め込もうとしているのでそりゃ長くもなるな〜と。
ただ命名だけで表現しようとするにも限界があるようで以下のように挙げられていました。

  • Propsの型の変更に合わせてコンポーネント名も変更させる必要がある
    • 保守性が低下
  • 関連するデータが肥大化、多くのコンポーネントを内包した場合、命名が複雑になる

命名だけでコンポーネントと型の関係を維持しつつ、ネストされたデータ構造を表現するのは厳しい課題を解決するために、このセッションではコンパニオンオブジェクトパターンとnamespaceを活用するアプローチが紹介されていました。
TypeScriptでは値と型が区別されており、同名で使うことができる特徴があります。これをコンパニオンオブジェクトパターンと呼びます。
https://typescriptbook.jp/tips/companion-object
これを利用して、先ほどまでコンポーネント名とProps名を区別していましたが、コンポーネントは値、Propsは型として扱われてるため、同名にして管理しやすくします。
またコンポーネントごとにnamespaceを用意することで、データ型の構造をモジュールで表現することができます。これによって構造的な命名を行うことができるというTipsでした。

namespaceを使うことで構造的に管理しやすくなる点はReactなどと親和性が高いと感じました。
命名で表現するよりも、より直感的に管理できそうですね。

構造的型付けと serialize 境界

https://speakerdeck.com/takonda/structural-subtyping-and-serialize
このセッションではシリアライズ境界で、TypeScriptの方の特徴である構造的型付けによって起きた問題とその対応策について紹介されていました。
問題提起として、TSの構造的型付けが原因で型定義にないプロパティが含まれているパラメータを渡すと、それも込みで処理されて期待するアウトプットとは異なる結果が得られるというバグが実際起きていたようです。
構造的型付けは構造が同じであれば(互換性があれば)、型にないプロパティがあっても許容する性質があります。
https://typescript-jp.gitbook.io/deep-dive/type-system/type-compatibility#gou-zao-de-structual
これにより本来不要なプロパティが型チェックをすり抜けて(許容されて)処理に反映されていたようです。この問題の解決策としては以下が挙げられました。

  • 直前で安全な値に変換する。(必要なプロパティのみ抽出し新しいオブジェクトで渡す)
  • Zodでバリデーションをかけて不要なプロパティを落とす

しかし、上記の方法では型チェックをすり抜けているには変わりないので、より根本的な解決方法として以下の方法が紹介されていました。

  • privateフィールドを持つクラスで型を実装
  • Brand hackを用いる

どちらもTSで名前的型付けとして定義する方法で、型をガチガチに担保していました。
※名前的型付けは構造が同じでも型の名前が異なれば別の型として扱われる。

名称型付けは構造的型付けよりも安全に扱える反面実装が複雑になりそうです。zodなどのバリデーションで十分対応可能であればそこに止めるでも良いと思いました。

as(型アサーション)を書く前にできること

https://speakerdeck.com/marokanatani/as-xing-asasiyon-woshu-kuqian-nidekirukoto

ここでは、anyの濫用がよくないという風潮が広まってきているが、asについては適切でない場面でも使われていると感じているという話から始まりました。
asの濫用が良くない理由としてコンパイラの挙動を上書きしてしまうためです。よくコンパイラよりも型を理解している場合に使いがちでしたが、チームメンバーでは異なる解釈をしている可能性もあるため、避けれるなら避けたいとのことでした。(確かに...)
型アサーションを避ける(書く前にできること)として以下を挙げていました。

また、インターフェース境界(外部APIとの連携部分)での型アサーションには注意が必要で、型にプロパティが生えても、アサーションを使っている以上型システムで判断が行われず、エラーが発生しないため避けたいとのことでした。
インターフェース境界で型アサーションを避ける(書く前にできること)として以下を挙げていました。

※型制約はプロパティが全て一致していないとエラーになるが、satisfiesでは互換性があれば追加プロパティを許容するという違いがある。

まとめとしては、型アサーションはTSの型システムの利点を損ない、型安全ではなくなるため、asを見つけたらそれはなぜ必要なのか、必要になる根本の原因は何かを考え、必要な場合はスコープを最小限に収めて実装することが大切とのことでした。

型アサーション便利で多用していましたが、型システムに頼らないため、最終手段で使うことに止める認識を持つことができました。

感想

実務から得られた教訓やTips、言語レベルの仕様のようなディープな話題まで幅広い発表でとても勉強になりました。特に型に関する解像度は上がった気がします。また、初めて知る単語や考え方が多くあったので、とてもいい刺激になり今後も吸収していきたいと思いました。

Discussion