TypeScriptのパフォーマンスを上げる

TypeScript Performance ガイド

https://github.com/microsoft/TypeScript/wiki/Performance

概要

このページは、TypeScript のコンパイル速度や VS Code の型チェック速度が遅い場合の改善方法をまとめた公式ドキュメントです。

Microsoft が最初に強調しているのは、

「まずはコードや設定を改善するだけで大きく高速化できる」

という点です。

また現在は TypeScript 本体を Go に移植した TypeScript 7.0(TypeScript Go) の開発も進んでおり、ケースによっては最大 10 倍程度高速になると説明されています。

まず押さえるべき重要ポイント

1. interface を優先し、巨大な intersection (&) を避ける

Microsoft が明確に推奨しています。

避けたい例

type User = BaseUser &
  AuditInfo & {
    name: string;
  };

推奨

interface User extends BaseUser, AuditInfo {
  name: string;
}

理由:

  • interface はフラットなオブジェクト型になる
  • 型関係の計算結果をキャッシュできる
  • プロパティ衝突を検出できる
  • intersection は毎回マージ処理が発生する
  • 複雑になると never が発生しやすい

Microsoft は

interface の型関係はキャッシュされるが、intersection 型全体はそうではない

と説明しています。


Type と Interface の性能論争について

この Wiki が言っているのは、

「interface が常に速い」ではありません。

正確には

「オブジェクト型を合成するときは intersection より extends の方がコンパイラに優しい」

という話です。

つまり、

type User = {
  id: string;
  name: string;
};

interface User {
  id: string;
  name: string;
}

の比較ではなく、

type User = A & B & C;

interface User extends A, B, C {}

の比較です。([GitHub][1])

そのため、

「Type は遅い」

という解釈は正確ではありません。


2. 関数の戻り値型を明示する

function fetchUser() {
  return {
    id: "1",
    name: "John",
  };
}

function fetchUser(): User {
  return {
    id: "1",
    name: "John",
  };
}

メリット

  • 型推論コスト削減
  • .d.ts 生成が軽くなる
  • Incremental Build が速くなる

特に大規模プロジェクトでは効果が出やすいとされています。


3. 巨大な Union 型を避ける

避けたい例

type Element =
  | DivElement
  | SpanElement
  | ImgElement
  | ...
  | VideoElement;

Union の各要素同士を比較する必要があり、

  • 比較回数が増える
  • 二乗オーダーになりやすい

という問題があります。


推奨

interface HtmlElement {
  id: string;
}

interface DivElement extends HtmlElement {}
interface ImgElement extends HtmlElement {}

共通の基底型を作る方が効率的です。


4. 複雑な Conditional Type に名前を付ける

避けたい例

interface Service<T> {
  execute<U>(
    value: U
  ): U extends A
    ? ResultA
    : U extends B
      ? ResultB
      : ResultC;
}

毎回 Conditional Type を評価する必要があります。


推奨

type ExecuteResult<U> =
  U extends A
    ? ResultA
    : U extends B
      ? ResultB
      : ResultC;

interface Service<T> {
  execute<U>(value: U): ExecuteResult<U>;
}

名前付き型にするとコンパイラがキャッシュしやすくなります。


5. Project References を活用する

大規模プロジェクトでは

client
server
shared

のように分割することが推奨されています。

メリット

  • 一度に読み込むファイル数を減らせる
  • VS Code が軽くなる
  • 増分ビルドが効く

Microsoft の経験則では、

5〜20 プロジェクト程度

がバランスが良いとのことです。


6. include / exclude を見直す

実際にはこれが原因で遅くなっているケースが非常に多いです。

よくある事故

{
  "include": ["**/*"]
}

これで

node_modules
dist
coverage

まで読み込んでしまう。


7. 不要な @types を読み込まない

推奨

{
  "compilerOptions": {
    "types": []
  }
}

必要なものだけ追加。

{
  "compilerOptions": {
    "types": ["node", "mocha"]
  }
}

これにより型解決コストを削減できます。


8. incremental を有効にする

{
  "compilerOptions": {
    "incremental": true
  }
}

.tsbuildinfo を利用して差分ビルドを行います。


9. tsc --extendedDiagnostics を使う

コンパイルが遅いときはまずこれ。

rm -f tsconfig.tsbuildinfo && tsc --extendedDiagnostics

# またはこちらでもok
tsc --extendedDiagnostics --incremental false

例:

Files: 1200
Types: 85000
Check time: 12.5s

確認できる項目:

項目 内容
Parse time パース時間
Bind time シンボル構築
Check time 型チェック
Emit time 出力
Memory used メモリ使用量

どこが遅いか特定できます。


チーム向けの実践的な結論

TypeScript の公式ガイドを踏まえると、

普段は

type User = {
  id: string;
  name: string;
};

でも

interface User {
  id: string;
  name: string;
}

でもほぼ問題ない。


ただし

複数型の合成では

type User = A & B & C;

より

interface User extends A, B, C {}

を推奨。

No.2750
06/11 13:03

edit