はじめに
こんにちは!iimonでエンジニアをしているひがです。
最近AIでソースコード生成を利用する場面も当たり前になってきましたが、その中でどんなコードを書くとAIはどんな反応をするのだろう??という疑問がふつふつと湧いてきまして、いてもたってもいられなくなってきました。
ということで、今回はTypeScriptの書き方でAIにどのような影響があるかを簡単に見ていこうと思います!とはいえ TypeScript の型システムはとても奥が深く、細かい内容を見ていくととても膨大になってしまうため、今回は Branded TypesとEffect TS に絞ってみていくことにします。
Branded Types
まず前提のお話として、開発を進めるにあたり TypeScript が提供する型システムには大きな恩恵があるかと思います。(コード品質を高めたり、思わぬバグを未然に防いだりなど助かることが多いですよね) ただ、その型システムも完璧というわけではなく、思わぬ落とし穴があることも多々あります。 一例として下記のコード例を見てみましょう。
type HigaType = { from: "okinawa"; profession: "engineer"; favoriteDrink: "orion"; iq: number; }; type TkushiType = { from: "okinawa"; profession: "engineer"; favoriteDrink: "orion"; iq: number; }; const getHigaIntro = (higa: HigaType) => { console.log(` Hello Everyone ! わたしの出身は${higa.from}でっす! 職業はスーパー${higa.profession}でっす! 好きな飲み物は${higa.favoriteDrink}でっす! IQは${higa.iq}でっす!`); }; const tKushisan: TkushiType = { from: "okinawa", profession: "engineer", favoriteDrink: "orion", iq: 200, }; getHigaIntro(tKushisan); // → output // Hello Everyone ! // わたしの出身は okinawa でっす! // 職業はスーパー engineer でっす! // 好きな飲み物は orion でっす! // IQは200 でっす!
悲劇です。ひがさんの自己紹介のはずなのに、Tkushi さんが同じ共通点を持っているばかりに意図せずコンパイルも実行時もエラーが発生せず、正常に実行されてしまいました。(ひがさんは IQ200 もあるはずがありません) これが TypeScript の型システム思わぬ落とし穴の一例です。
TypeScript では構造が同じであれば同一の型であるとみなしてしまう性質があります。これは構造的型付け(structural typing)と呼ばれ、型の構造によって型を区別しているという特徴を持ちます。この性質は JavaScript のダックタイピングを満たせるように型システムが構築された経緯があり、より柔軟な実装を可能にしている性質でもあります。ちなみに、Java など厳密に静的解析する言語では名前的型付けが採用されており、これは構造ではなく型の名前で型を分別しています。(上記の HigaType と TkushiType は別の型として判定されます)
下記に詳細がありますので、気になる方はぜひ見ていってください! https://typescriptbook.jp/reference/values-types-variables/structural-subtyping
構造的型付けは柔軟で使い勝手が良い側面がある一方、意味的には意図しない処理のまま開発が進んでしまい、そのままリリースされてしまうリスクも秘めています。特に認証まわりなど大事な部分で意図しない処理で進むと致命傷になるケースもあり、ちゃんと理解していないと結構危険な性質となっています。
このような悲劇を回避する手段の一つとして Branded Types という手法があります。その名の通り通常の型にその型を特定するためのプロパティ(ブランド)を付与することで、構造は同じでも型として区別できるようになります。
上記の実装例で Branded Types を表現すると下記のようになります。
type HigaType = { from: "okinawa"; profession: "engineer"; favoriteDrink: "orion"; iq: number; } & { readonly _brand: "higa" }; type TkushiType = { from: "okinawa"; profession: "engineer"; favoriteDrink: "orion"; iq: number; } & { readonly _brand: "tKushi" }; ...(略)... const tKushi: TkushiType = { from: "okinawa", profession: "engineer", favoriteDrink: "orion", iq: 200, _brand: "tKushi", }; getHigaIntro(tKushi); // → output コンパイルエラー // Argument of type 'TkushiType' is not assignable to parameter of type 'HigaType'. // Type 'TkushiType' is not assignable to type '{ readonly _brand: "higa"; }'. // Types of property '_brand' are incompatible. // Type '"tKushi"' is not assignable to type '"higa"'.
無事に Tkushi さんとひがさんは区別されるようになり、思わぬ悲劇は回避できるようになりました。
あくまで簡単な一例ですが、Branded Types を使うことで意味論的にも明確にコードを書くことができるようになります。特にジェネリクスなど抽象化して書きたいときとかでもある程度意味を持たせることができるので、書き方の工夫次第でより品質が高く保守しやすいコードになっていく可能性を秘めています。
ここまでメリットを中心に見てきましたが、デメリットも存在し、特に大きなデメリットとしてはコード量の増加に伴う可読性の低下があります。
なんでもかんでも Branded Types にしてしまうと純粋に見づらくなってしまうことがありますので、使い所は考えて実装する必要がありました。
た・だ・し ☆
AI の登場により状況は変わってくるかもしれません。
生成 AI で大量のコードが瞬時に生み出される環境下ではAI が解釈しやすいコードを書くことがとても大切だと思います。特に先でも触れましたが、生成 AI が生み出す大量のコードに対してレビューに苦労しているところも多いかと思いますが、AI が問題なく解釈してくれる土台が整うのであれば人によるレビューの負荷もある程度下げることができるかもしれません。
Branded Types はそのような可能性を多いに秘めている手法だと思います。
Effect TS
いきなりですが、みなさんは Effect TS をご存知でしょうか?
Effect TS は TypeScript で堅牢なアプリを作るためのツールキットで、エラーハンドリングや DI(依存性の注入)、リソース管理、並行処理などを実用的に解決するための機能が詰まっています。
公式サイト: https://effect.website/
ドキュメント: https://effect.website/docs/introduction
GitHub: https://github.com/Effect-TS/effect
例えばよく Error を throw したけど try catch を忘れてた!!!みたいな感じでうっかりする瞬間もあるかと思います。(あと、そもそもtry catchの書き方って冗長だったりネストすると見づらくなってしまったりしますよね)
Effect TSは関数型プログラミングの思想をベースにそのようなうっかりを型レベルで解決してくれるとても優れた設計となっています。
Effect TS の基本単位は Effect<A, E, R> という型で表現され、それぞれ
A(Success): 処理が成功したときに返却される値の型 E(Error): 処理の中で発生する可能性のあるエラーの型 R(Requirements): 処理を実行するために必要な依存関係
を表しています。
つまり「この処理には R が必要で、成功すれば A を、失敗すれば E を返却する」という情報を型として表現します。
とりあえずまぁ、簡単な一例を見て行きましょう。
import { Effect, Context, Schedule, Cause } from "effect"; // 1. エラー型の定義(型安全のために独自のクラスを作ると扱いやすいです) class RandomError extends Cause.RuntimeException { readonly tag = "RandomError"; } // 2. サービスの定義 class Random extends Context.Tag("MyRandomService")< Random, { readonly next: Effect.Effect<number, RandomError> } // E に RandomError を追加 >() {} // 3. ロジックの実装 const program = Effect.gen(function* () { const random = yield* Random; // サービスから値を取得 const randomNumber = yield* random.next; console.log(`取得した数値: ${randomNumber}`); }); // 4. 実装の提供(非同期・遅延・エラー・リトライの注入) const runnable = program.pipe( Effect.provideService(Random, { next: Effect.gen(function* () { // 1 秒の遅延を入れる(非同期処理) yield* Effect.sleep("1 seconds"); const val = Math.random(); if (val < 0.5) { console.log(`失敗... (${val.toFixed(3)})`); return yield* Effect.fail(new RandomError("0.5未満のためエラー")); } console.log(`成功! (${val.toFixed(3)})`); return val; }), }), // リトライの設定:失敗したら 最大 5 回まで試行 Effect.retry(Schedule.recurs(5)), ); // 実行 Effect.runPromise(runnable) .then(() => console.log("すべての処理が完了しました")) .catch((err) => console.error("最終的なエラー:", err)); // → output // 失敗... (0.497) // 失敗... (0.478) // 失敗... (0.384) // 成功! (0.776) // 取得した数値: 0.775972828803668 // すべての処理が完了しました
上記の例を見てみると下記の変数は次のような型を保持しています。
program: Effect.Effect<void, RandomError, Random>; runnable: Effect.Effect<void, RandomError, never>;
runnable は非同期・遅延・エラー・リトライなどを管理しつつ、program の処理を呼び出しています。program はカスタマイズされたエラー型(RandomError)を持つように Random クラスの依存が注入されています。
最終的な処理としては 0.5 以上の数字が出るまで最大 5 回までランダム値生成を繰り返し、処理が完了したらログ出力するという処理になっており、より実践的な処理を Effect という体系を立てて実装することができます。
よく型定義やバリデーション、フィルタリングロジックなどはばらけて定義されることも多いかと思いますが、Effect TS を利用することで、一通りの流れの処理を型レベルで統一して表現することができます。この性質は AI にとって処理の流れをより理解しやすくし、仕様に対してより正確にコードを生成することに繋げることができます。
デメリットとしてはEffect TS は独自の書き方やルールも多く、人にとっても AI にとっても学習コストがかかることが一番大きいかと思います。 (関数型プログラミングなどの概念理解も含めて)
た・だ・し ☆
こちらもAIによって状況が変わってくるかもしれません。
まだ Unstable ではありますが、AI 用のパッケージ(effect/ai など)が用意されていたり、LLM のための docs(llms.txt など)が用意されていたりするので、今後もますます AI と共に発展していきそうな香りが漂っています。
https://effect.website/blog/effect-ai/
https://effect.website/docs/getting-started/introduction/#docs-for-llms
あと、Effect TSの中にはBranded Typesを考慮されたBrand型なども用意されており、そのほかにも様々な便利機能が用意されているので、使い方によってはAI(と人)が保守しやすいコードの生成の可能性を秘めています。
まとめ
ここまで TypeScript の実装が AI にどのような影響を与えるかについて簡単に見てきました。TypeScript(だけに限らずその他の言語もそうですが)にはまだまだ様々な性質があり、それらを加味して実装を工夫することで、AI が誤解せず仕様を忠実に再現し運用保守も困らない開発の未来が見えてくるかもしれません。また、これらの実装の工夫をベースに、ts-morphなどでCompiler API を利用し、よりAIが正確に仕様と実装状況を理解して有用なスキルを生成することもできるかもしれません。このあたりも合わせて、引き続き実験しようと思うこの頃でした。
さいごに
弊社では現在エンジニアを募集しています! この記事を読んで少しでも興味を持っていただけた方、ぜひカジュアルにお話ししましょう!!
iimon採用サイト / Wantedly / Green
参考
https://typescriptbook.jp/reference/values-types-variables/structural-subtyping
https://effect.website/
https://effect.website/docs/introduction
https://github.com/Effect-TS/effect
https://effect.website/blog/effect-ai/
https://effect.website/docs/getting-started/introduction/#docs-for-llms