TypeScript 7.0のnative compilerと7.1 Betaの新API
native compilerの並列化と既定設定、7.1 Betaのimport attributes、source phase import、新しいcompiler APIを解説します。
TypeScript 7.0は2026年7月8日に正式公開されました。 7.0ではcompilerがGoによるnative実装へ移り、parsing、type checking、emitを並列に処理します。 10月20日には、importの新機能と新しいcompiler APIを加えた7.1 Betaも公開されています(7.0の公式発表、7.1 Betaの公式発表)。
tscの実装と、生成するJavaScriptを分けて考える
native化されたのは開発時に動くcompilerです。 TypeScriptのプログラムをGoの実行ファイルへ変換する機能ではなく、従来と同じくJavaScriptを出力します。 discriminated unionを使う例では、型検査後もcaseごとの分岐がJavaScriptへ残ります。
type Result = { kind: "ready"; value: number } | { kind: "missing" };
function describe(result: Result): string {
switch (result.kind) {
case "ready":
return `value=${result.value}`;
case "missing":
return "missing";
}
}
console.log(describe({ kind: "ready", value: 42 })); // => value=42
TypeScript 7.0.2とNode.js 26.12.0でのcompileと実行は、次のcommandに対応します。
tsc --strict --target es2024 --module nodenext --outDir dist main.ts
node dist/main.js
Resultの型情報は出力から消え、switchと文字列を返す処理がJavaScriptへ残ります。
並列化のうち、type checkingにはworkerごとの型情報が必要です。 7.0はworkerを既定で4個作り、各workerに担当するファイルを決まった方法で割り当てます。 共有する依存型の計算は重複する場合があるため、workerを増やすとmemory使用量も増え得ます(7.0のtype checkingの並列化)。

--buildでproject referenceを処理する場合は、--buildersが同時にbuildするproject数を制御します。 たとえば--builders 4 --checkers 4なら、最大16個のcheckerが動き得ます。 --singleThreadedはcheckerを1個にするだけでなく、parsingとemitも単一threadにします。 7.0.2のtsc --help --allは、3個のoptionと既定値を表示します。
worker数の変更で、型情報の評価順に依存する結果も変わる場合があります。 同じ入力に対する再現性を揃える場合、--checkersの値まで同じにします。 --buildersによる並列実行もprojectの依存関係に従います。 個数を増やしても、依存先の完了前に後続のprojectを実行できません(並列buildの条件)。
fileごとに分けられる処理と、共有する型へ依存する処理
parsingは、fileの文字列を構文木へ変換する処理です。 あるfileの構文を読むために、別のfileの型検査をすべて終える必要はありません。 emitも、fileごとに処理を分けられる部分があります。 対してtype checkingでは、importした型、global scope、genericな型の計算結果など、fileを越える情報を使います。 source fileをCPU数と同じ個数に分けるだけでは、共通のdependencyを各threadで繰り返し調べる処理が増えるため、段階ごとに異なる並列化が必要です(7.0の処理段階と並列化)。
checker workerは、型を扱う独自の状態を持ちます。 共通の型計算が重複する一方、同じfile集合を同じworker数で処理する場合には、担当の分割を固定します。 型を必要とした順序によって結果が変わる経路を持つため、単に空いたworkerへ次々とfileを渡す構成とは異なります。 処理を並べる順序の自由度と、既存の型検査の再現性を同時に扱うための分割です。 --checkersは、workerの個数だけでなく、各workerが持つ型情報の分割にも影響します。
workerを8個へ増やすと、4個の場合より広く並列に処理できる場合があります。 ただし、共有するdependencyを調べる量や、型情報を保存するmemoryも増え得ます。 fileが少ないprojectや、同じ巨大な型へ依存するfileが多いprojectでは、worker数と処理時間が比例する条件にはなりません。 CPUのcore数だけでなく、型のdependencyの重なりが並列化の効率を決めます。
project referenceの並列化は、dependency graphの幅に制限される
--buildersが扱うのは、--buildで選択する複数projectの関係です。 たとえば基礎libraryのAをBとCが参照し、アプリケーションDがBとCを参照する構成では、BとCを同時に進められる段階があります。 一方、AからB、BからC、CからDという1本の連鎖なら、project数が4個あっても、同じ幅の並列性はありません。 builderの個数を増やしても、依存する宣言の準備が完了していないprojectを、依存関係を無視して先へ進める設定ではありません(project reference builderの適用範囲)。
project内のchecker数と、同時に動くproject数は掛け合わさります。 --builders 4 --checkers 4は、compiler全体でcheckerを4個に限る指定ではありません。 最大4個のprojectで4個ずつcheckerを使えるため、最大16個になります。 CIの外側でもjobを複数並べる場合には、同時に起動したcompilerごとに同じ構成ができます。 個々のprojectで適度な設定でも、複数projectや外側のjobへ重ねると、総memoryの条件が変わります。
--checkers 1は、type checkingのworkerを1個にする指定です。 --singleThreadedは、parsingとemitも含めて単一threadへ制限します。 したがって、type checkingを1個へ制限しても、compiler全体を単一threadへ制限したことにはなりません。 外側のtask runnerで並列性を管理する場合や、単一thread同士を比較する場合には、制限する段階が異なります。 --buildersはproject間、--checkersはproject内の型検査を制御します。 --singleThreadedはcompiler全体のthread利用を制限します(single-threaded mode)。
native化しても、出力先のJavaScript runtimeは別に選ぶ
ResultはTypeScriptの型で、実行時のtagを自動で検査するobjectではありません。 出力されたJavaScriptには、result.kindを読むswitchが残り、型aliasの宣言は残りません。 native化によって型情報がGoのruntimeへ移り、アプリケーションの実行時にも検査される構成ではありません。 外部から受け取る値をResultとして使う前のvalidationも、compile時の型検査だけでは代行されません。 compilerの実装言語、sourceの型表現、生成したプログラムが動くruntimeは、別々の対象です。
--targetは出力するJavaScriptの構文と、既定で読むlibrary宣言の選択に関わります。 compiler自身がnative executableになっても、古いJavaScript runtimeに存在しないAPIを自動で実装する機能は加わりません。 同様に、--moduleが決めるmoduleの表現と、実際のfileを読み込むNode.jsやbundlerの解決処理も異なります。 compilerの更新だけで、生成先のruntimeやpackageのmodule構成まで一括して更新されるわけではありません(targetの設定、moduleの設定)。
既定設定と、従来のcompiler APIへの依存
7.0は6.0で導入された既定設定を引き継ぎ、6.0でdeprecatedだった設定や構文をerrorにします。 strictとnoUncheckedSideEffectImportsは既定で有効です。 moduleはesnext、rootDirは./が既定になり、stableTypeOrderingは無効にできません(TypeScript 6.0の移行に関する変更)。
--watchも実装が変わり、Parcelのfile watcherをGoへ移した実装を使います。 依存ファイルを繰り返しpollingする方式から、OSのfile notificationを利用する方式へ移すための変更です(7.0のwatch mode)。
compilerをlibraryとして呼ぶツールでは、CLIの置き換えとAPIの移行が別になります。 従来のtypescript packageのcompiler APIが必要な用途には、6系を提供する@typescript/typescript6が用意されています。 7.1 Betaで導入されるAPIは、従来のAPIのimport先だけを変える互換APIではありません(7.0のAPIと互換package)。
rootDirの既定値は、出力先のdirectory構成へ現れる
rootDirは、出力するfileの相対pathを計算する基準です。 tsconfig.jsonの隣にsrcがあり、sourceがsrc/index.tsにあるprojectでは、基準をproject rootにするかsrcにするかで、出力のpathが変わります。 outDirがdistなら、project rootを基準にするとdist/src/index.js、srcを基準にするとdist/index.jsという差になります。 sourceのimport文を1行も変えなくても、生成物を参照するpackageのentry pointや実行commandに影響する変更です(rootDirと出力の関係)。
rootDirは、compile対象のfileを選別するための設定ではありません。 入力を選ぶfiles、include、excludeと、出力時の相対pathを決める基準は役割が違います。 従来の出力構成を保存したいprojectでは、実際に基準にしていたdirectoryをrootDirとして明示する変更になります。 出力先でsrcが1段増えた場合に、compilerがsourceを余計に読み込んだ結果だと決め付けると、変更すべき設定を取り違えます。 7.0で変わる既定値は、明示済みの設定を上書きするものではありません。
7.0.2では、src/index.tsとoutDir: "dist"だけを持つprojectにTS5011を出します。 共通のsource directoryが./srcであるため、rootDirを明示するように求める診断です。 noEmitOnErrorを有効にしていない場合は、exit code 2でもdist/src/index.jsを生成します。 --rootDir layout/src --outDir layout/dist-explicitを明示すると、生成先はdist-explicit/index.jsになります。 出力fileが存在する事実だけでは、compilerが成功したことまで判定できない例にもなっています。
errorがあるbuildから新しいJavaScriptを出したくない場合は、noEmitOnErrorを有効にします。 既定値はfalseで、errorがあってもemitする動作を許します。 trueでは、JavaScriptだけでなくsource mapやdeclaration fileの出力も抑止します(noEmitOnErrorの対象と既定値)。 出力pathを決めるrootDirと、emitの可否を決めるnoEmitOnErrorは、異なる工程へ作用します。 rootDirを直せばTS5011の原因を除けますが、別の型errorが残る場合のemit条件まで変更しません。 逆に、noEmitOnErrorだけを有効にしても、TS5011が要求する基準directoryは決まりません。 pathの設定を移行する処理と、不正なbuildの生成物を出さない設定を、必要な動作へ合わせて指定します。
typesの既定値も空の配列へ変わっています。 可視範囲の@types packageを自動的にglobalへ取り込む前提のprojectでは、Node.jsの名前やtest用のglobalを解決できなくなる場合があります。 types: ["node"]は、Node.js向けの型をglobalへ取り込む指定です。 types: ["*"]は従来の探索へ戻す経路ですが、個別のpackageを列挙する指定とは、取り込む集合が異なります。 空の既定値は、明示したimportのmoduleを型検査できなくする設定ではなく、globalへ自動で含める型の集合に関わります(typesが制御する範囲)。
strict=trueの既定値は、以前に省略していたstrict系の検査を有効にします。 すでにstrictをtrueにしていたprojectでは、既定値だけを理由に検査がもう一段強くなるわけではありません。 一方、省略によってfalseを得ていたprojectは、引数の暗黙のanyやnullの扱いなど、検査する条件が変わり得ます。 strictをfalseにする明示的な設定は残るため、更新時に増えた診断がsourceの変更によるものか、既定値によるものかを分けられます(strictがまとめる検査)。
noUncheckedSideEffectImportsは、値を束縛しないimport "./setup"のようなimportも、解決できるか検査する設定です。 値を使用していないことと、moduleを実行するための依存が存在しなくてよいことは異なります。 CSSなどをbundlerが扱うprojectでは、対応するambient declarationも解決の入力になります。 ただし、ambient declarationへ一致しても、実fileの存在まで検証した結果にはなりません。 7.1 Betaのattributes付き宣言にも同じ条件が残ります(noUncheckedSideEffectImports)。
削除されたoptionは、6.0のdeprecation抑制では復活しない
7.0.2は--target es5 --noEmit main.tsにTS5108を出し、target=ES5が削除されたと報告します。 --baseUrl .には、削除されたoptionを示すTS5102を出します。 両方とも型検査の途中でsourceの型が合わなくなった結果ではなく、compilerのoptionとして受け付けない入力です。 6.0で使えたignoreDeprecations: "6.0"は、7.0で削除した機能を再実装する指定ではありません。 deprecationを一時的に抑える段階と、optionが存在しない段階は、buildの失敗する位置が異なります(6.0から7.0へ持ち越されない設定)。
ES5向けのtargetとdownlevelIterationは、古い構文や反復の変換に関わっていました。 moduleではAMD、UMD、SystemJS向けの値が削除対象です。 module resolutionではclassicとnode10が対象になります。 同じNode.jsという名前でも、node10の探索規則は現在のpackage exportsなどを扱うnodenextと同一ではありません。 生成するmodule形式と、dependencyを解決する規則の両方が、従来の設定の移行対象です(module resolutionの設定)。
baseUrlの削除は、path aliasの文字列を、出力されたJavaScriptへ自動で変換する機能を追加しません。 pathsはcompilerがmoduleを探す際の対応付けであり、runtimeのimport specifierを書き換える設定ではありません。 aliasを維持する場合には、compilerが探す対象と、runtimeやbundlerが解決する名前の対応が残ります。 baseUrlの削除は、すべてのbare specifierの意味を変更する操作ではありません(pathsとemitの関係)。
outFileも削除対象で、compiler自身が対応したmodule形式を1個のfileへまとめる経路は使えなくなります。 さらに、esModuleInterop=false、allowSyntheticDefaultImports=false、alwaysStrict=falseという古い設定も対象です。 source上では、namespaceを表す旧式のmodule構文、import assertionのassert構文、no-default-lib directiveが対象になります。 7.0は6.0の型検査へ近付けたnative実装です。 ただし、6.0でdeprecationを無視できた機能も、削除の対象になります(6.0でdeprecatedとなった機能)。
型の表示順序に関わるstableTypeOrderingも、7.0では固定された動作になります。 従来のcompilerには、型を最初に見つけた順序で内部IDを割り当て、IDでunionの構成要素を並べる処理がありました。 直接関係しない定数をfileの前へ置いただけでも、declaration fileへ出すunionの順序を変えてしまう実装でした。 6.0のstableTypeOrderingは、7.0へ移行したときの順序へ合わせるための設定です。 型の集合として同じunionでも、生成した宣言を文字列として比較する処理には差が現れる対象になります(6.0のstable type ordering)。
file watcherの実装を、型検査の並列性と独立させる
watch modeでpollingを使う場合は、変更の有無を確かめるため、一定の間隔で多数のfileを調べます。 node_modulesを含む大きいprojectでは、変更のないfileを繰り返し調べる処理も増えます。 7.0のwatcherは、ParcelのC++実装をGoへ移した独立のpackageを基礎にし、OSの変更通知を利用します。 compilerの型検査を速くする処理とは別に、変更を検出するための継続的な負荷を扱う変更です(7.0のwatcherの実装)。
Goの標準libraryには、必要なcross-platformのfile watching APIがありませんでした。 既存のC++実装を直接組み込む方式なら、Go以外のbuild toolchainが必要になります。 TypeScript側はGoへのportと小さいassemblyの補助を使い、C++のtoolchainを追加せずにwatcherをbuildする構成を選びました。 watchする対象を決めるcompiler側の判断と、変更通知を受けるplatform側の実装を分けています。 --checkersを増やす設定は、watch対象のfileをOSが通知する仕組みを置き換える指定ではありません。
7.1 Betaでは、native化した後の実装にも追加の最適化が入っています。 unionの構築、型情報のcache、ASTの重複fieldの整理、不要なallocationの削減など、処理する情報と保存する情報の両方が対象です。 公式の--noEmit、checker 4個の比較では、mui-docsの時間が22.2秒から11.9秒、memoryが8093 MiBから4079 MiBになっています。 一方、blueskyの時間は4.48秒から3.89秒で、同じ比率ではありません。 入力に含まれる型計算や構文の分布によって、内部実装の削減が総量へ現れる割合も異なる結果です(7.1 Betaの公式測定)。
測定の比較対象は、7.0と7.1 Betaのcompilerです。 --noEmitなので、JavaScriptの出力やbundlerの処理を含むbuild全体の所要時間でもありません。 compilerの型検査が占める範囲へ対する改善と、別の工程を含むCI全体の時間を同じ割合で減らす結果は区別されます。 native compilerの内部改善と並列化の設定は、開発時の処理へ作用します。 アプリケーションのruntimeは、生成したJavaScriptを実行する段階の性能へ作用します。
7.1 Betaではimport attributesからmoduleの型を決める
従来のdeclare module "*.txt"は、ファイル名の拡張子だけでmoduleの型を宣言していました。 しかし、同じファイルをtextとして読むかbyte列として読むかは、runtimeやbundlerに渡すimport attributesでも変わります。 7.1 Betaではambient module declarationにもwithを書き、attributesに対応した型を宣言できます(実装PR #63931)。
declare module "*" with { type: "text" } {
const content: string;
export default content;
}
declare module "*" with { type: "bytes" } {
const content: Uint8Array;
export default content;
}
宣言をassets.d.tsに置くと、同じ名前のファイルから異なる型をimportできます。
import text from "./missing.asset" with { type: "text" };
import bytes from "./missing.asset" with { type: "bytes" };
const title: string = text;
const data: Uint8Array = bytes;
型検査の条件は7.1.0-betaの--strict --noEmit --module esnext --moduleResolution bundlerです。 attributesなしのimportは、一致する宣言がなくTS2307になります。
import missing from "./missing.asset";
// TS2307: Cannot find module './missing.asset' or its corresponding type declarations.
attributesを条件にする宣言へ解決できなかったためです。
ambient moduleに一致したimportでは、ファイルの存在を検証せず、宣言した型を与えます。 型検査を通った後の読み込みや、textとbytesの変換はruntimeやbundlerが行います(ambient moduleへの解決と存在確認の範囲)。
拡張子の分類と、読み込む表現の選択を分ける
bundlerのCSS Modulesと、ブラウザーのCSSStyleSheetでは、同じ.cssから返す値の形が異なります。 file名のpatternだけで常にCSSStyleSheetだと宣言すると、attributesを付けなかった別の読み込みも同じ型になります。 7.1 Betaのattributes付きambient declarationは、file名だけでは足りなかった条件を、moduleの型を選ぶ入力へ含めます。 type: "text"とtype: "bytes"を分けて宣言すると、同じspecifierへ別の型を付けられます(PR #63931)。
declare module "*" with { type: "text" }の*は、特定の拡張子へ限定しないpatternです。 .txtと.mdの両方を文字列として読む環境なら、拡張子ごとの宣言を同じ内容で複製せずに済みます。 一方、type: "bytes"の宣言へ一致したimportは、文字列のmethodを持つ値にはなりません。 保持するのがdecode済みの文字列か、decode前のbyte列かを型として区別します。 TypeScriptが同じfileを2回読み、内容から適切な型を推論した結果ではありません。
missing.assetが存在しなくても、textとbytesのimportはambient declarationへ一致します。 attributesを外したimportは、宣言と実fileのいずれへも解決できず、TS2307を出します。 解決できた事実が保証するのは、型を与える宣言との対応です。 packaging時にfileを含め忘れた失敗や、runtimeが未知のattributeを拒否する失敗を、ambient declarationだけで検出するわけではありません。
attributesに対応する型宣言は、7.1の組み込みlibraryへ一律に含まれるものではありません。 利用するruntimeやbundlerが返す型に合わせて、project側の宣言として定義します。 たとえばDOMの型を含めないNode.js向けprojectへ、CSSStyleSheetの宣言を無条件に追加した場合です。 globalのDOM型も必要になります。 attributes名が同じでも、読み込み方式の異なる環境では、宣言する型を合わせる必要があります(7.1 Betaのambient declarationの提供範囲)。
source phase importはmoduleを実行する前の段階を取り出す
7.1 Betaはimport sourceとimport.source()にも対応します。 通常のimportがmoduleを読み込み、依存関係を結び、実行した結果を扱うのに対し、source phaseでは解析後かつ依存先の読み込み前のmodule sourceを受け取ります。 WebAssemblyならWebAssembly.Moduleを得て、必要なimportsを渡しながらinstanceを作る使い方ができます(実装PR #63915)。
対応するmoduleはesnext、nodenext、preserveで、型定義にはesnextが必要です。 TypeScriptで構文を受け付けることと、実行環境がmodule sourceを扱うことは別の条件です。 JavaScriptのmodule sourceを後から実行する機能まで、7.1 Betaのcompilerが代行するわけではありません(source phase importの適用範囲)。
module sourceと、instanceのexportを区別する
通常のimportで使うmodule namespaceには、moduleを評価した後のexportが現れます。 source phase importが受け取るのは、解析されたmodule sourceを表すobjectです。 依存moduleを読み込み、linkし、module内のcodeを実行する段階へ進む前に取得します。 同じspecifierでも、返るobjectの表す処理段階は異なります。 sourceを取得した時点では、moduleのtop-levelは未実行です(source phase importを実装したPR #63915)。
WebAssemblyでは、sourceのobjectがWebAssembly.Moduleです。 moduleをinstance化する際に、必要なimport objectを渡せます。 同じmodule sourceから、異なるimportやmemoryの設定を渡して複数のinstanceを作る構成が可能です。 module sourceをcacheする対象と、実行時の状態を持つinstanceをcacheする対象を分けられます。 source objectは、通常のmodule namespaceとは異なります。 exportされたfunctionの取得には、instance化の処理が必要です(Node.jsのWasm source phase imports)。
staticなsource phase importで許可するのはdefault importです。 import source { operation } from ...のように、moduleのexportから個別の名前を取り出す構文ではありません。 dynamicな形にはimport.source(specifier)を使います。 通常のimport()が返すmodule namespaceと同じ型として扱うのではなく、module sourceを返す操作として型付けします。 TypeScriptのglobalな型では、AbstractModuleSourceが対応する入口になります。
source objectはidentityを保存します。 同じmodule sourceを複数回importした値同士は、MapやSetのkeyとして比較する対象になります。 一方、同じsourceからinstance化した値は、個別の実行時の状態を持つ別のinstanceです。 sourceのidentityの一致は、instanceのidentityの一致を意味しません。 解析済みの入力を再利用する関係と、実行時のinstanceを共有する関係が、別の操作として表れます(ECMAScriptのsource phase imports proposal)。
JavaScript moduleのsourceを任意の環境で再実行する仕組みまで、今回のTypeScriptが完成させるわけではありません。 compilerは構文をparseし、型を与え、選択したmodule設定に応じて出力を扱います。 module sourceを取得し、後続の処理へ渡す機能は、runtime側の対応にも依存します。 WebAssemblyのinstance化の用途と、将来のJavaScript moduleの制御に関するproposalを、現在すべて同じ操作が使える機能としてまとめることはできません。 7.1 Betaの追加範囲は、source phaseをsourceに記述できる言語処理と型の入口です。
新APIではGo側のresourceの生存期間を閉じる
7.1 Betaのcompiler APIは、同期clientのtypescript/syncと非同期clientのtypescript/asyncに分かれます。 ASTの型やfactory、file systemを表すAPIも別のentry pointです。 同期APIは、作成したprogramに属するsemantic diagnosticを返します(7.1 Betaのcompiler API)。
import { API } from "typescript/sync";
using api = new API();
const options = api.parseCommandLine([
"--strict", "--noEmit", "--target", "es2024", "check-input.ts",
]);
using program = api.createProgram(options.fileNames, options.options);
const diagnostics = program.getSemanticDiagnostics();
console.log(diagnostics.map((diagnostic) => diagnostic.code)); // => [ 2322 ]
check-input.tsには、文字列を整数の型へ代入する宣言を置きます。
export const answer: number = "forty-two";
API clientのcompile条件は7.1.0-betaと--lib esnext,dom、runtimeはNode.js 26.12.0です。 TS2322は、文字列からnumberへの代入の不一致を表します。
APIやProgramは、JavaScript側のobjectに対応するresourceをGo側にも保持します。 JavaScriptのGCだけではGo側のresourceを解放できないため、usingでscopeの終了時に破棄します。 snapshotを更新するAPIでも、更新前と更新後のprogramは別のsnapshotに属するobjectとして扱います(7.1 Betaのresource管理とsnapshot)。
CLIと同じ設定の読み込みを、自動では行わないAPIの呼び出し
parseCommandLineへ渡したfile名とoptionから、Programを作ります。 tsc -p projectと同じように、周囲のtsconfig.jsonを自動探索する実装にはしていません。 したがって、同じdirectoryでCLIとAPIを呼んだ事実だけでは、同じ入力集合とcompiler optionを使う保証にはなりません。 CLIが読み込むproject設定と、APIへ渡すoptionを明示する処理を対応付けて初めて、同じ条件の検査になります。 configの探索を省略したAPIの例では、入力の違いによって型検査の意味も変わります(7.1 Betaのbatch compilationの例)。
diagnosticも1種類ではありません。 global、program、syntactic、semanticのdiagnosticは、異なる処理段階の問題を表します。 getSemanticDiagnostics()は型の代入などのsemanticな問題を返し、globalやsyntacticなdiagnosticをまとめて返す入口ではありません。 独自のbuild toolを作る場合、検査する段階を省略した結果と、diagnosticが0件だった結果を混同しない構成が必要です。 CLIの出力を再現する要求と、特定の型情報を照会する要求では、APIで組み立てる処理の範囲が異なります。
同期clientと非同期clientは、同じ名前を別のlibraryへ置いたものではありません。 非同期clientはPromiseに基づく呼び出しを提供し、同期clientは呼び出しの結果を同期的に取得します。 ASTの型やenumはtypescript/ast、type guardはtypescript/ast/is、構築と更新のfactoryはtypescript/ast/factoryに分かれます。 compilerを制御する操作と、構文木の型を参照する用途を、importする入口から分けています。 従来の巨大なroot exportを前提としたtoolのsourceは、必要な入口の選択とAPIの使い方を変更する対象です。
snapshotの更新後も、古いdiagnosticを読む
virtual fileの初期状態をexport const answer: number = "forty-two";とすると、整数への文字列の代入にdiagnosticを出します。 createFileSystemWithLibでsource fileを定義し、compilerの標準libraryも読めるfile systemを使っています。 同じfileをexport const answer: number = 42;へ更新するlayerは、新しいsnapshotにだけ適用されます。
import { ScriptTarget } from "typescript/ast";
import { createFileSystemLayer, createFileSystemWithLib } from "typescript/fs";
import { API } from "typescript/sync";
const fileName = "/virtual/report.ts";
using api = new API();
using before = api.createSnapshot({
fileSystem: createFileSystemWithLib([
[fileName, 'export const answer: number = "forty-two";'],
]),
createPrograms: [{
rootFiles: [fileName],
compilerOptions: { strict: true, noEmit: true, target: ScriptTarget.ES2024 },
}],
});
using beforeProgram = before.operation.createdPrograms[0];
console.log("before", beforeProgram.getSemanticDiagnostics().length); // => before 1
using after = before.update({
fileSystem: createFileSystemLayer([
[fileName, "export const answer: number = 42;"],
]),
ensurePrograms: [beforeProgram.id],
});
using afterProgram = after.getProgram(beforeProgram.id);
if (!afterProgram) throw new Error("Updated program missing");
console.log("after", afterProgram.getSemanticDiagnostics().length); // => after 0
console.log("before-again", beforeProgram.getSemanticDiagnostics().length); // => before-again 1
更新は、既存のprogramの内容を同じobjectの中で無条件に書き換える操作ではありません。 新しいsnapshotを作り、更新後のprogramを取得します。 programのIDはsnapshotを越えて対応付けられるため、更新後の状態で同じprogramの検査結果を要求できます。 一方、typeやsymbolなど、特定のsnapshotから得たobjectを別のsnapshotのcheckerへ混ぜる契約ではありません。 IDによる対応関係と、型情報のobjectを共有してよい範囲を分ける設計です。
ensureProgramsには、新しいsnapshotで取得するprogramのIDを指定します。 snapshotの更新を重ねるだけの経路では、すべてのprogramを即座に再計算する必要がないためです。 after.getProgram(beforeProgram.id)で更新後のdiagnosticを取得するため、同じIDのprogramを確保します。 fileの変更を積み上げる操作と、programの型検査を要求する操作を別に扱います。 編集を続けるtoolでは、毎回すべての入力を最初から作る構成より、状態の遷移として表せる範囲が広がります(snapshot APIの更新とprogramの確保)。
usingの宣言は、後で作ったresourceから先に破棄します。 usingで宣言したprogramとsnapshotの破棄が終わると、最後にAPIを閉じます。 JavaScript側の変数がscopeを抜けた事実だけで、Go側が保持するresourceを即座に解放できるとは限らないため、明示した破棄の経路があります。 破棄後のsnapshotへ再び照会する使い方はできません。 更新前の状態を保持できる性質も、保持するresourceの生存期間内で使う契約です(explicit resource managementの破棄順序)。
virtual file systemの完全な置換と、layerによる上書き
createFileSystem(files)は、指定したfileを持つ完全なfile systemを定義します。 列挙していないfileは、明示してmountしない限り存在しません。 createFileSystemWithLib(files)は、同じ完全なfile systemへ、標準libraryのdirectoryを読み取り専用でmountします。 sourceの内容だけをmemory上で与えても、ArrayやPromiseなどの型宣言へアクセスする条件を用意できます。 hostのdiskへ一時fileを書き出す処理と、compilerへ見せるfile systemを定義する処理が、API上で分かれています(file system API)。
createFileSystemLayer(files)は、指定したfileだけを上書きし、残りをbaseのfile systemへ委ねます。 createFileSystemLayerへ渡すentryは、変更したsource fileの内容だけです。 標準libraryを全部書き直す必要がないのは、baseへ委ねる経路があるからです。 完全なfile systemを毎回置き換える操作と、変更部分だけを重ねる操作は、未指定のfileが存在するかの規則から異なります。 disk上のfileを消したのではなく、照会先のfile systemが返す状態を更新しています。
ASTのupdateは、元のnodeを直接書き換えない
item.beforeを表すproperty accessのnodeへ、同じreceiverと同じ名前でupdateを適用すると、元のnodeを返します。 名前をafterへ変えて更新すると、元とは異なるnodeが返ります。 元のnodeの名前はbeforeに保たれ、新しいnodeだけがafterになります。 変更がない場合の再利用と、変更がある場合の新しい構造の作成を、同じupdate APIで扱っています。
import { NodeFlags } from "typescript/ast";
import {
createIdentifier,
createPropertyAccessExpression,
updatePropertyAccessExpression,
} from "typescript/ast/factory";
const receiver = createIdentifier("item");
const name = createIdentifier("before");
const original = createPropertyAccessExpression(receiver, undefined, name, NodeFlags.None);
const unchanged = updatePropertyAccessExpression(original, receiver, undefined, name);
const changed = updatePropertyAccessExpression(original, receiver, undefined, createIdentifier("after"));
console.log(unchanged === original, changed === original); // => true false
console.log(original.name.text, changed.name.text); // => before after
factoryの更新は、既存のnodeのpropertyへ代入して内容を変えるAPIとは異なります。 元の構文木を参照する処理が残っていても、新しいnodeを作っただけで元の名前が書き換わる結果にはなりません。 ただし、factoryで構文上のnodeを作ったことは、programの意味解析を更新した結果ではありません。 構文木の構築と、fileを更新してsnapshotのcheckerから型を取得する処理は別の入口です。 ASTの作成だけでは、更新後のsemantic diagnosticを計算した結果になりません(typescript/ast/factoryの位置付け)。
library宣言とconditional typeの検査も変わる
7.1 Betaはtargetとlibのes2026に対応します。 Math.sumPrecise、Iterator.concat、JSON.rawJSON、MapのgetOrInsertなどの宣言が対象です。 esnextには、まだproposalであるPromise.allKeyedやIterator.zipなどの宣言も追加されています(7.1 Betaのlibrary宣言)。
ES2026の宣言があっても、runtimeへAPIは追加されない
7.1.0-betaの--target es2026 --module nodenextでは、Math.sumPreciseの呼び出しを型検査できます。 しかしNode.js 26.12.0には、同じmethodの実装がありません。
Math.sumPrecise([1e16, 1, -1e16]);
// TypeError: Math.sumPrecise is not a function
compilerがmethodの宣言を知っていることと、対象のJavaScript runtimeがmethodを実装していることは、独立した条件です。 出力にもMath.sumPrecise(...)という呼び出しが残り、補助実装へ置き換わってはいません。
同じruntimeでも、Iterator.concatとJSON.rawJSONは呼び出せます。
console.log([...Iterator.concat([1, 2], [3])]); // => [1, 2, 3]
console.log(JSON.stringify({ value: JSON.rawJSON("9007199254740993") }));
// => {"value":9007199254740993}
JSON.rawJSONは、指定した数値の字句をJSONの値として保持します。 JavaScriptのnumberへ一度変換してから、丸めた値を文字列へ戻す経路ではありません。 入力には有効なraw JSONの値が必要であり、任意の文字列を無条件に認めるAPIではありません(JSON.rawJSONのECMAScript仕様)。
Map.getOrInsertは、keyが存在しないときに指定した値を挿入します。 同じkeyへ異なる値を渡しても、既存の値が優先されます。
const counts = new Map<string, number>();
console.log(counts.getOrInsert("key", 1)); // => 1
console.log(counts.getOrInsert("key", 9)); // => 1
let computed = 0;
console.log(counts.getOrInsertComputed("key", () => ++computed)); // => 1
console.log(computed); // => 0
既存の値を毎回上書きするsetと同じ操作ではありません。 getOrInsertComputedは、すでにkeyが存在する場合、値を作るfunctionを呼びません。 値を先に計算してから渡す方式と、keyがない場合にだけfunctionを呼ぶ方式の違いが現れています(Map upsertの仕様)。
WeakMapにも同じ名前のmethodの宣言が入り、Uint8ArrayにはBase64とhexの変換、ArrayにはfromAsyncなどがES2026へ含まれます。 libを選択する操作は、sourceが参照できる型宣言の集合を変えます。 targetで古い構文へ変換しても、runtimeへlibraryのmethodを追加するわけではありません。 Node.js 26.12.0はIterator.concatを提供しますが、Math.sumPreciseは提供しません。 7.1のES2026対応は、型宣言を安定した年次の選択肢へまとめる変更であり、すべてのruntimeへ同時に同じ機能を入れる変更ではありません。
型検査では、分配されるconditional typeの各分岐に対する制約が厳密になりました。 B extends Aでも、Aが0 | 1、Bが0なら、Bは分配後のA = 1を満たしません。 7.1は分配前の制約を、分配後のすべてのmemberにも成り立つ制約として扱わなくなります(分配されるtype parameterの検査)。
type Contained<Whole extends number, Part extends Whole> = Part;
type Select<Whole extends number, Part extends Whole> =
Whole extends Part ? never : Contained<Whole, Part>;
type Counterexample = Select<0 | 1, 0>;
公式例の型名を変えた断片は、7.0.2では受け付けますが、7.1.0-betaではTS2344を出します。 Whole = 0の分岐はneverになりますが、Whole = 1の分岐にはContained<1, 0>が残ります。 0は1を満たさないため、分配前のPart extends Wholeだけでは後者の利用を正当化できません(公式発表の元の例)。
分配前のunionに対する制約は、各memberへの制約にならない
元の制約は、PartがWholeというunion全体へ代入できるという条件です。 0は0 | 1へ代入できますが、unionの各memberへすべて代入できるという条件は満たしません。 conditional typeの分配でWholeを0と1へ分け、元の包含関係を両方へコピーすると、1の分岐で誤った関係ができます。 7.1では、分配したparameterを、元のparameterより狭い別の型として扱います。 別のparameterとの関係を無条件には引き継ぎません。 診断は、型aliasの定義で要求した制約と、分配後に利用する制約の不一致を検出しています(分配されるconditional typeの規則)。
修正する方法は、Containedへ渡したい型がunion全体なのか、分配したmemberなのかで異なります。 union全体を維持したい場合は、分配用に別のMemberを用意できます。 Member extends Partで分岐しながら、Contained<Whole, Part>には分配していないWholeを渡す構成です。 PreserveWhole<0 | 1, 0>は、7.1でも値0を代入できる型になります。 分配して調べる対象と、包含関係を保証する対象を、同じparameter名へ重ねない修正です。
type PreserveWhole<
Whole extends number,
Part extends Whole,
Member extends Whole = Whole,
> = Member extends Part ? never : Contained<Whole, Part>;
const preserved: PreserveWhole<0 | 1, 0> = 0;
一方、各memberについて本当に包含関係を要求したい場合は、分岐内でPart extends Wholeを検査します。 条件を満たした分岐だけでContained<Whole, Part>を使えば、分配後のmemberにも必要な包含関係が成立します。 RequireEach<0 | 1, 0>では、0の分岐も1の分岐も結果がneverになります。 union全体を保存する修正とは、受け付ける分岐と結果の型が異なります。 warningを消すための同じ書き換えとして扱うと、元の型aliasが表したかった結果を変える場合があります。
[Whole] extends [Part]のようにtupleで囲む方法は、conditional typeの分配を止める別の変更です。 分配した各memberの結果を集める計算と、union全体を1回で判定する計算は異なります。 7.1の診断に対して、常にtupleで囲めば元の意味を保存できるという規則はありません。 型の分岐をmemberごとに残すのか、全体の条件へ変えるのかを、型aliasの入力と期待する結果で決めることになります。