TypeScript Meets Go: 10x TypeScriptを理解する
Ethan Miller
Product Engineer · Leapcell

TypeScriptからGoへの移行の詳細な探求:決定、利点、そして将来の見通し
I. プロジェクトの背景と起源
(I) プロジェクトコードネームの起源
新しいTypeScript移行プロジェクトのコードネームはCorsaです。古いコードベースであるStrataは、かつてTypeScriptの最初のコードネームであり、2010年末または2011年初めに内部開発段階で始まりました。最初のチームは、Steve Lucco、Anders Hejlsberg、Lukeで構成されていました。Steveは、Internet ExplorerのJavaScriptエンジンのスキャナーとパーサーを抽出および修正して、元のプロトタイプコンパイラーを作成しました。これは、概念実証に使用されたC#のコードベースでした。
(II) 変更を推進するパフォーマンスの問題
ECMAScriptコミュニティでは、esbuildやswcなど、高度にツールに依存するプロジェクトをネイティブコードに移行するのがトレンドです。TypeScriptは、パフォーマンスとスケーラビリティの問題に直面しています。プロジェクトが成長し続けるにつれて、コンパイラーはV8とJavaScriptエンジンにますます圧力をかけます。新機能の追加により、起動時間が長くなります。以前の最適化では、5%〜10%の改善しか得られず、パフォーマンス最適化の限界に達しました。
II. Go言語を選択する理由
(I) Rust言語との比較
- メモリ管理と互換性: 既存のTypeScriptコードベースは、自動ガベージコレクションの存在を前提としています。ただし、Rustのメモリ管理は自動ではありません。そのボローチェッカーは、データ構造の所有権に厳密な制約があり、循環データ構造を禁止しています。TypeScriptのデータ構造(抽象構文木(AST)など)は、循環参照を広範囲に使用します。Rustに移行するには、データ構造を再設計する必要があり、大きな困難が伴います。したがって、Rustは基本的に除外されます。
- 開発者の経験と学習コスト: JavaScriptからGoへの移行は、Rustよりも簡単です。いくつかの点で、GoコードはJavaScriptコードに似ています。Rustの複雑な構造または再帰的な構造を扱う場合、TypeScriptコードからの進化プロセスを理解するのがより困難です。人的資源の観点から、Goを選択する方が利点があります。
(II) C#言語との比較
- 言語設計の方向性: Goは、ネイティブコードをより優先する言語です。自動ガベージコレクションの機能があり、データ構造のレイアウトとインライン構造の点で表現力が豊かです。C#は、ある程度バイトコード指向です。事前コンパイルはありますが、すべてのプラットフォームで利用できるわけではなく、最初からネイティブパフォーマンスの最適化を目標として設計されていませんでした。
- プログラミングパラダイムの違い: TypeScriptのJavaScriptコードベースは、高度な関数型プログラミングスタイルを採用しており、コアコンパイラーはクラスをほとんど使用しません。Goも関数とデータ構造に焦点を当てています。対照的に、C#は主にオブジェクト指向プログラミング(OOP)です。C#に移行するには、プログラミングパラダイムを切り替える必要があり、移行の摩擦が増加します。
(III) Go言語の有利な適合性
Go言語は、すべての主流プラットフォームで優れた最適化されたネイティブコードを提供できます。循環データ構造とインラインデータ構造を可能にする、データ構造に対する優れた表現力を持っています。自動ガベージコレクションと共有メモリへの同時アクセス、および優れたツールチェーンとVS Codeなどのツールからの優れたサポートの機能を備えています。TypeScriptの移行の多面的なニーズを満たし、多くの言語の中で際立っています。
III. プロジェクトが直面する課題と解決策
(I) ブートストラップの放棄のトレードオフ
ブートストラップ言語とは、それ自体で書かれた言語です。TypeScriptは以前はブートストラップ言語でした。Goに移行した後、ブートストラップを放棄することについて懸念がありますが、10倍のパフォーマンス向上のために、チームは依然として移行を選択しています。ただし、JavaScriptで記述された一部のパーツ(言語サービスパーツなど)は保持されます。チームは、ネイティブ部分(Go)と他の言語のコンシューマーとの間にAPIを構築するためのソリューションを検討しています。
(II) 互換性を確保するための取り組み
TypeScriptには公式の仕様がなく、リファレンス実装は仕様に似ています。Goに移行する場合、セマンティックな一貫性を維持する必要があります。チームの目標は99.99%の互換性であり、同じコードベースに対してまったく同じエラーを生成することを望んでいます。現在、オープンソースのコンパイラーは、Visual Studio Codeのすべてをコンパイルおよびチェックでき、クラッシュせずに20,000の適合性テストを実行できます。チームは、エラーベースラインを分析し、違いを排除し、古いコンパイラーのプラグアンドプレイの代替品になることを目指しています。
(III) 型ソートの決定性の問題
古いコンパイラーは、単純な非決定的な型ソート方法を使用していました。これは、シングルスレッド環境では決定的でしたが、マルチスレッドの同時実行環境では非決定的でした。新しいコードベースは、決定的な型ソートを導入する必要があり、場合によっては古いコンパイラーとは異なる型順序につながります。特に、共用体の順序は一部のシナリオで重要であり、チームはこれらの問題の解決に取り組んでいます。
(IV) API設計のジレンマ
古いコードベースのコンパイラーの内部構造のほとんどすべてがAPIとして公開されました。新しいコードベースは、APIを再設計し、プロセス間通信中のAPIの効率を確保することを検討する必要があります。現在、チームは新しいコードベースにバージョニング可能で最新のAPIを提供する方法を検討しています。
IV. プロジェクトにおける並行性の応用と利点
(I) コンパイラーの関数型プログラミング基盤は並行処理を促進します
TypeScriptコンパイラーは元々、関数型プログラミングモデルを採用し、安全な共有を保証するために不変性を広範囲に使用していました。たとえば、スキャン、解析、バインド後のASTは、基本的に不変と見なされます。複数の型チェッカーが同じASTを同時に処理できるため、JavaScript自体に共有メモリの同時実行メカニズムがなくても、同時処理の優れた基盤が提供されます。
(II) 解析段階での並行性の実装
解析タスクは、並列化に非常に適しています。各ソースファイルの解析作業は、完全に独立して完了できます。たとえば、5000個のソースファイルと8つのCPUがある場合、ファイルを8つの部分に分割し、各CPUが1つの部分を処理できます。共有メモリ空間では、完了後、すべてのデータ構造を構築およびリンクする部分が実行されます。Goで解析段階の並行性を実装するのは非常に簡単です。おそらく、goroutineで操作を実行するために約10行のコードが必要なだけであり、同時に、mutexを使用して共有リソースを保護することができます。これにより、パフォーマンスを3〜4倍向上させることができます。
(III) 型チェック段階の並行性スキーム
型チェッカーはプログラムのグローバルビューを必要とするため、解析プロセスのように完全に独立することはできません。チームは、プログラムをいくつかの部分(現在はハードコードされて4つ)に分割し、4つの型チェッカーを作成します。各チェッカーは、割り当てられたファイルの部分をチェックします。これらは、基になる不変のASTを共有し、独自の型状態を構築します。この方法では、約20%多くのメモリを消費しますが(型の重複が原因)、約3倍のパフォーマンス向上が得られます。ネイティブコードによってもたらされる3倍のパフォーマンス向上と組み合わせると、全体的なパフォーマンス向上は10倍に達する可能性があります。
V. TypeScriptの将来の見通し
(I) 言語機能の開発動向
現在、ECMAScriptの開発速度は低下しています。コミュニティからのフィードバックは、人々が型システムの新しい派手な機能よりも、スケーラビリティとパフォーマンスに関心を持っていることを示しています。TypeScriptチームは、ECMAScript委員会の活動に注意を払い、型システムの新しい機能を適切に処理すると同時に、型チェッカーの10倍の速度向上と新しい可能性の探求の影響について検討します。
(II) 人工知能との組み合わせの可能性
高速型チェッカーを使用して、型解決の結果、シンボルの宣言場所など、大規模言語モデル(LLM)のコンテキスト情報を提供します。AIの出力をリアルタイムでチェックして、そのセマンティックな正確性を確保し、AIが安全で信頼性の高いコードを生成するための保証を提供し、新しい開発パスを開きます。
(III) ネイティブランドタイムの構想
TypeScriptのネイティブランドタイムが可能かどうかを検討します。現在、Rustで記述されたDenoがあります。オブジェクトモデルや数の処理方法など、パフォーマンスに影響を与えるJavaScriptの要素がいくつかありますが、TypeScriptのネイティブランドタイムを作成するには、多くの不確実な要素があり、今後の開発方向はまだ不明です。
VI. サードパーティの貢献とコミュニティへの影響
JavaScriptからGoへの移行は、システムにとっては比較的穏やかです。JavaScriptのみを知っている人よりも、GoとJavaScriptの両方を知っている人は少ないため、貢献者の数が減少する可能性がありますが、コンパイラーに貢献する人の数は元々多くなく、通常はネイティブ環境に足を踏み入れることに関心があります。Go言語はシンプルであり、そのシンプルな設計により、10倍のパフォーマンス向上などの目覚ましい成果が得られており、コミュニティの活力と発展を妨げることはありません。
VII. TypeScriptとGo言語の一般的なステートメントの比較
(I) ループ
- TypeScript(JavaScriptベース)
for
ループ:
for (let i = 0; i < 10; i++) { console.log(i); }
for...of
ループ(配列などの反復可能なオブジェクトを反復処理するために使用されます):
const arr = [1, 2, 3]; for (const num of arr) { console.log(num); }
for...in
ループ(主にオブジェクトのプロパティを反復処理するために使用されます):
const obj = { a: 1, b: 2 }; for (const key in obj) { console.log(key, obj[key]); }
- Go言語
for
ループ(Go言語には基本的なループ構造が1つしかありませんが、for
ループを使用すると、さまざまなループ方法を実装できます):
for i := 0; i < 10; i++ { fmt.Println(i) }
- 配列やスライスなどの反復可能なオブジェクトを反復処理します:
arr := []int{1, 2, 3} for index, value := range arr { fmt.Println(index, value) }
- マップを反復処理します:
m := map[string]int{"a": 1, "b": 2} for key, value := range m { fmt.Println(key, value) }
(II) 関数
- TypeScript
- 関数の定義:
function add(a: number, b: number): number { return a + b; }
- アロー関数:
const multiply = (a: number, b: number): number => a * b;
- Go言語
- 関数の定義:
func add(a int, b int) int { return a + b }
- 匿名関数(変数に割り当てるか、パラメーターとして渡すことができます):
multiply := func(a int, b int) int { return a * b }
(III) オブジェクト指向プログラミング(OOP)
- TypeScript
- クラス定義:
class Animal { name: string; constructor(name: string) { this.name = name; } speak() { console.log(`${this.name} makes a sound.`); } }
- 継承:
class Dog extends Animal { constructor(name: string) { super(name); } speak() { console.log(`${this.name} barks.`); } }
- Go言語
- Go言語には、クラスと継承の従来の概念はありません。構造体とメソッドセットを通じてOOPと同様の機能を実現します。
- 構造体の定義:
type Animal struct { Name string } func (a *Animal) Speak() { fmt.Printf("%s makes a sound.\n", a.Name) }
- 合成を通じて継承に似たものを実現します:
type Dog struct { Animal } func (d *Dog) Speak() { fmt.Printf("%s barks.\n", d.Name) }
(IV) 関数型プログラミング
- TypeScript
- 高階関数の例(関数をパラメーターとして受け入れます):
function operateOnArray(arr: number[], callback: (num: number) => number): number[] { return arr.map(callback); } const result = operateOnArray([1, 2, 3], num => num * 2);
- イミュータブルなデータ構造は、外部ライブラリ(Immutable.jsなど)の助けを借りて実装できます。 例:
import { fromJS } from 'immutable'; const list = fromJS([1, 2, 3]); const newList = list.push(4);
- Go言語
- 高階関数の例:
func operateOnSlice(slice []int, callback func(int) int) []int { result := make([]int, len(slice)) for i, v := range slice { result[i] = callback(v) } return result } result := operateOnSlice([]int{1, 2, 3}, func(num int) int { return num * 2 })
- Go言語自体は、一部の関数型プログラミング言語のようなイミュータブルなデータ構造をネイティブにサポートしていません。 ただし、構造体などをコピーしてデータのイミュータビリティを確保するなど、いくつかの設計パターンとライブラリを通じてイミュータブルな動作をシミュレートできます。
Reference: https://www.youtube.com/watch?v=pNlq-EVld70&ab_channel=MicrosoftDeveloper
Leapcell: Webホスティング、非同期タスク、およびRedis用の次世代サーバーレスプラットフォーム
最後に、Goサービスのデプロイに最適なプラットフォームをお勧めします。Leapcell
1. 複数言語のサポート
- JavaScript、Python、Go、またはRustで開発します。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い — リクエストも料金もありません。
3. 無敵のコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60ミリ秒で694万リクエストをサポートします。
4. 合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOpsの統合。
- 実用的な洞察を得るためのリアルタイムのメトリクスとロギング。
5. 容易なスケーラビリティと高性能
- 容易に高い並行性を処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ — 構築に集中するだけです。
Leapcell Twitter: https://x.com/LeapcellHQ