プロローグ — 2026年のTypeScriptは二語
"We are rewriting tsc in Go. It is ten times faster." — Anders Hejlsberg、2025年3月11日の発表ノート
2025年3月、Anders Hejlsberg が TypeScript チームの1年の仕事を公開した — tsc を Go で書き直したポート、通称 tsc-go。同じコードベース上で、型チェック10.4倍、ビルド10.5倍、メモリ半分。 Microsoft 自身のコード(150万行)で76秒→7.5秒。
そして同じ年、Explicit Resource Management プロポーザルが ECMAScript Stage 3 で安定し、TypeScript は using と await using 宣言を本格出荷した。Java の try-with-resources、Python の with、C# の using が、ようやく JavaScript にも入った。
この二つが2026年の TypeScript を定義する。コンパイラは書き直された。言語はリソース管理を手に入れた。
その間に静かに腰を据えたものも少なくない。
- デコレータが Stage 3 から ES2025 に標準化され、
@accessorや@methodのパターンがライブラリで定着した。 satisfies演算子は「5.0の新機能」から「標準語彙」になった。NoInfer<T>、const型パラメータ、infer extendsで型推論が一段精緻になった。isolatedDeclarationsでモノレポビルドが速くなった。- エディタの tsserver が体感で速くなった。
この記事は、それらすべてを 日々書くコード の視点から見直す。どの機能が実際にコードを変えるのか、どの機能が単に格好いいだけなのか。率直に線を引く。
第1章 · tsc-go ポート — 「10倍」が意味すること
まず事実から。
何がどう書き直されたか
従来の tsc は TypeScript 自身で 書かれていた — コンパイラが自分自身をコンパイルする。優雅だが遅い。V8 上で JIT/GC が回り、巨大なグラフ(型・シンボル・ノード)を扱うため、メモリも厚く食う。
tsc-go は Go でゼロから書き直した 別実装だ。同じ AST、同じ型システム、同じ診断メッセージを出すように設計されている。挙動は同じ、ランタイムが違う。
[従来の tsc] [tsc-go]
TypeScript Go
V8 / JIT ネイティブコンパイル
GC ポーズ GC は短く、並列
ほぼシングルスレッド マルチスレッド志向
メモリ ~2倍 メモリ ~半分
大きなモノレポ 60-120秒 大きなモノレポ 6-12秒
Microsoft 発表のベンチマーク:
- VS Code コードベース: 77.8s -> 7.5s (10.4x)
- Playwright: 11.1s -> 1.1s (10.1x)
- TypeORM: 17.5s -> 1.3s (13.5x)
- date-fns: 6.5s -> 1.0s (6.5x)
何が速くなり、何がそのままか
速くなるもの:
tsc --noEmitの型チェック — もっとも直接的な恩恵。- エディタの応答性 (tsserver) — Go ベースの tsserver-go が hover・go-to-definition・補完を体感で速く返す。
- CI のビルド時間 — node_modules を引いた後の型チェック段階。
- モノレポのプロジェクト参照ビルド — 依存グラフを辿る段の並列化。
そのままのもの:
- バンドル時間 — esbuild・swc・rolldown が別に走る領域。tsc-go が直接速くするわけではない。
- ランタイムの挙動 — 出力 JS は同じ。
- 型システムそのもの — 同じルール、同じ診断。互換性は意図的。
@types/*パッケージ — DefinitelyTyped 生態系と完全互換。
2026年の採用状況 (現実的に)
tsc-go は2025年にプレビュー公開され、2026年初頭に正式リリース(typescript-go または tsc --go フラグ)に入った。TypeScript 6 は両方のコンパイラを同梱で出荷する。
tsc— 既存の JS 実装、互換性保証。tsc-go— 新しい Go 実装、新規推奨。
しばらくは二つが共存する。診断を一致させるために大きな努力が払われたが、細部の差が出るケースはある(だからこそ二つを同梱している)。大きなコードベースを移すときは、両方のコンパイラを並走させて診断を差分比較する のが標準手順になった。
実務で何が変わるか
CI で一行を変えるのが最大の変化だ。
# 従来
npx tsc --noEmit
# 2026
npx tsc-go --noEmit
# または
TSC_BACKEND=go npx tsc --noEmit
モノレポでは tsc -b ビルドが分単位から秒単位に落ちる。これが一番体感する変化だ。「型チェック待ち」が事実上消える。
エディタでは hover・補完・diagnostic 表示が目に見えて速くなる。VS Code には typescript-go をバックエンドに選ぶ設定が入った。
採用の価値 — 率直な答え
ほぼ全員が採用する。 リスクはほぼゼロ(同じ診断・同じ出力)。報酬は大きい(10倍)。共存中は互換性も保証される。唯一の落とし穴 はビルドスクリプトが tsc を強く想定している場合 — --listFiles のような副次フラグの出力が細かく違うことがある。
第2章 · using と await using — リソース管理が言語に入った
ECMAScript Stage 3 の Explicit Resource Management プロポーザルが2025年に安定し、TypeScript は using キーワードを本格出荷した。ES2024 に入り、ES2025 で固まる流れだ。
何が問題だったか
リソース(ファイルハンドル・DB接続・ミューテックス・トランザクション)は 必ず閉じられなければならない。JavaScript にはそれを保証する言語機能がなかった。try/finally が標準のイディオムだったが、ネストと忘れが多い。
// 従来のパターン — try/finally 地獄
async function processFile(path: string) {
const handle = await fs.open(path, 'r')
try {
const lock = await acquireLock()
try {
const tx = await db.beginTransaction()
try {
// ... 本来の作業
} finally {
await tx.rollback()
}
} finally {
lock.release()
}
} finally {
await handle.close()
}
}
読みにくく、閉じる順序が構造で決まってしまう。
using が行うこと
using で宣言した変数は、スコープを抜けるときに Symbol.dispose(または Symbol.asyncDispose)を自動で呼ぶ。
async function processFile(path: string) {
using handle = await fs.openWithDispose(path, 'r')
using lock = await acquireLockWithDispose()
await using tx = await db.beginTransactionAsync()
// ... 本来の作業
// スコープ終了時に逆順で自動クリーンアップ: tx -> lock -> handle
}
Symbol.dispose・Symbol.asyncDispose はそれ自体が標準シンボルだ。リソースを持つものは誰でもこのメソッドを実装して using と協調できる。
class DbConnection {
async [Symbol.asyncDispose]() {
await this.client.end()
}
}
{
await using db = await DbConnection.connect(url)
await db.query('SELECT 1')
} // ここで db が自動で閉じる
DisposableStack — 動的なリソース束
リソースの数が動的なら DisposableStack/AsyncDisposableStack を使う。
async function pipeline(urls: string[]) {
await using stack = new AsyncDisposableStack()
const connections = await Promise.all(
urls.map(async (u) => {
const conn = await connect(u)
stack.use(conn) // dispose 時に一緒に閉じる
return conn
}),
)
// すべての作業のあと、スタックが解けて逆順に閉じる
}
本当に変わるコード
using が本当に変えるのは エラーフローが複雑なところ だ。二つ三つの場所で throw しうるコードは try/finally のネストが爆発する。using はそれを平らにする。
- ファイルシステムハンドル
- DB トランザクション・コネクションプール
- 一時ディレクトリ (
tmpDir) - ロック(分散ロックを含む)
- 計測 (
using timer = startSpan()) - テストのフィクスチャ(setup/teardown)
採用の価値 — 率直な答え
ライブラリから採用する。 リソースを抽象化するライブラリ(DB ドライバ・ファイル IO・ロギング)が Symbol.dispose/asyncDispose を実装すべきだ — そうしてはじめてユーザーは using を書ける。アプリケーションコードは自然に追随する。既存の try/finally を一気に書き換えるな — 新しいコードで使えば十分だ。
ランタイムの対応: Node 22 以上、Bun 1.1 以上、Deno 1.46 以上でポリフィルなしに動く。それ未満では TS がダウンレベリングしたコードを出すが、ランタイムが Symbol.dispose を知っていないとクリーンアップは走らない。
第3章 · Promise.withResolvers() — 外から解決される Promise
小さな機能だが毎日使う。ES2024 標準で、すべての主要ランタイムに入っている。
何が問題だったか
「外から解決される Promise」が欲しいとき、私たちはこのパターンを繰り返していた。
let resolve!: (value: string) => void
let reject!: (err: unknown) => void
const promise = new Promise<string>((res, rej) => {
resolve = res
reject = rej
})
button.addEventListener('click', () => resolve('clicked'))
button.addEventListener('error', (e) => reject(e))
const value = await promise
let resolve!: ... の断定が気になる。TS は resolve が必ず代入されることを知れない。
Promise.withResolvers()
const { promise, resolve, reject } = Promise.withResolvers<string>()
button.addEventListener('click', () => resolve('clicked'))
button.addEventListener('error', (e) => reject(e))
const value = await promise
型がきれい。意図が明確。一行短い。
どこで光るか
- 単発イベントを待つ — DOM イベント、EventEmitter、メッセージチャネル。
- キャンセル可能な処理 — 外側から
reject(new AbortError())でクリーンに終了。 - テストの同期 — 「このシグナルを待つ」の標準形。
- リアクタパターン — キューが空のあいだブロックするワーカーループ。
class Queue<T> {
private items: T[] = []
private waiters: Array<{ resolve: (v: T) => void }> = []
push(item: T) {
const waiter = this.waiters.shift()
if (waiter) waiter.resolve(item)
else this.items.push(item)
}
async pop(): Promise<T> {
if (this.items.length > 0) return this.items.shift()!
const { promise, resolve } = Promise.withResolvers<T>()
this.waiters.push({ resolve })
return promise
}
}
採用の価値
そのまま使う。 リスクはゼロ。ランタイムにどこでもある。新しいコードは無条件にこれを使う。
第4章 · satisfies とテンプレートリテラル型 — 「標準語彙」になった
5.0 で入った二つの機能は、2026年にはもう「高度な機能」ではない。すべてのコードに自然に混ざる。
satisfies — 推論を失わずに検査する
従来パターンの罠。
type Color = { hex: string; rgb: [number, number, number] }
// 1) アノテーション — 推論を失う
const palette: Record<string, Color> = {
red: { hex: '#ff0000', rgb: [255, 0, 0] },
green: { hex: '#00ff00', rgb: [0, 255, 0] },
}
palette.red.hex // type: string (具体キーが消える)
// 2) as — 安全性を失う
const palette2 = {
red: { hex: '#ff0000', rgb: [255, 0, 0] },
} as Record<string, Color>
palette2.blue.hex // コンパイル OK、ランタイム undefined
satisfies は両方を解決する。
const palette = {
red: { hex: '#ff0000', rgb: [255, 0, 0] },
green: { hex: '#00ff00', rgb: [0, 255, 0] },
} satisfies Record<string, Color>
palette.red.hex // type: '#ff0000' (リテラル保持)
palette.blue // コンパイルエラー — 存在しないキー
具体型はそのまま、制約は検証。 ルート定義・設定オブジェクト・タプルマッピングまですべてに入る。
テンプレートリテラル型 — ルートと API を型に結ぶ
ルート文字列があちこちに散らばるコードは必ず壊れる。テンプレートリテラル型でそれを一括にまとめる。
type Route =
| '/users'
| '/users/:id'
| '/posts'
| '/posts/:id/comments'
type Params<R extends string> =
R extends `${infer _}:${infer P}/${infer Rest}`
? { [K in P | keyof Params<`/${Rest}`>]: string }
: R extends `${infer _}:${infer P}`
? { [K in P]: string }
: {}
function navigate<R extends Route>(route: R, params: Params<R>) {
// ...
}
navigate('/users/:id', { id: '42' }) // OK
navigate('/users/:id', {}) // エラー — id 不足
navigate('/users/:userId', { id: '42' }) // エラー — :userId は Route にない
同じパターンが fetch ラッパー・tRPC・トレーシング・permission に入る。
type ApiMap = {
'GET /users': { params: void; result: User[] }
'GET /users/:id': { params: { id: string }; result: User }
'POST /users': { params: { body: NewUser }; result: User }
}
async function call<K extends keyof ApiMap>(
endpoint: K,
params: ApiMap[K]['params'],
): Promise<ApiMap[K]['result']> {
// ...
}
採用の価値
satisfies は どこでも 使う。as は最後の手段にとどめる。テンプレートリテラル型は 境界が明確な場所(ルート・API・イベント名)に限る — 一般コードで濫用するとコンパイルが遅くなる。
第5章 · デコレータ — Stage 3 から ES2025 へ
デコレータは長く険しい道を歩いた。2014年初提案、何度かの再設計、2022年に Stage 3 到達、そして ES2025 の一部 にようやくなった。TypeScript は 5.0 でこの新セマンティクスを本格導入し、2026年には「ふつうに使う機能」になった。
何が変わったか
- レガシーデコレータ (
experimentalDecorators) — TC39 の初期案ベース、Angular・NestJS・TypeORM が依存。 - 標準デコレータ (Stage 3 -> ES2025) — 呼び出しのセマンティクスが明確、型安全、
@accessorのような新カテゴリが追加。
新セマンティクスの下では、デコレータは次の5種類に付けられる。
- クラス自身
- メソッド
getter/setteraccessor(自動生成のゲッター/セッター + private フィールド)- フィールド
それぞれのデコレータは コンテキストオブジェクト を第二引数で受ける。名前・種類・addInitializer(インスタンス初期化後コールバック)を提供する。
実用例 — @logged
function logged<T extends (this: any, ...args: any[]) => any>(
target: T,
context: ClassMethodDecoratorContext,
): T {
const name = String(context.name)
return function (this: unknown, ...args: unknown[]) {
console.log(`-> ${name}(${args.join(', ')})`)
const ret = (target as any).apply(this, args)
console.log(`<- ${name} =`, ret)
return ret
} as T
}
class Calculator {
@logged
add(a: number, b: number) {
return a + b
}
}
new Calculator().add(2, 3)
// -> add(2, 3)
// <- add = 5
accessor — 新しいカテゴリ
function trace<T>(
target: ClassAccessorDecoratorTarget<unknown, T>,
context: ClassAccessorDecoratorContext,
): ClassAccessorDecoratorResult<unknown, T> {
return {
get() {
const value = target.get.call(this)
console.log(`read ${String(context.name)} =`, value)
return value
},
set(v) {
console.log(`write ${String(context.name)} =`, v)
target.set.call(this, v)
},
}
}
class Counter {
@trace
accessor value = 0
}
accessor キーワードは、コンパイラが private フィールドとゲッター/セッターを自動生成することを意味する。デコレータがその両方をフックする。
ライブラリ生態系
- NestJS 11+ — 標準デコレータへの全面移行(2025年進行中、フラグで両方をサポート)。
- TypeORM 0.4+ — 標準セマンティクスへの移行が進行中。
- MobX 7 — 最初から新セマンティクスの上にビルド。
- tRPC — デコレータを使わない方向を堅持。
採用の価値 — 率直な答え
必要な場所だけ使う。 メタプログラミングが似合う場所(ORM・DI・検証・ロギング・メトリクス)ではデコレータが理にかなう。それ以外は関数のほうがほぼ常に良い。experimentalDecorators フラグが点いているレガシーは新セマンティクスへ漸進移行 — 大きな仕事だが報酬も大きい(型安全・標準互換)。
第6章 · 型ナローイングの進化 — NoInfer、const 型パラメータ、infer extends
TypeScript の推論はバージョンごとに精緻になる。2026年時点で日常的に使う三つ。
NoInfer<T> — 推論の方向を止める
関数シグネチャが同じ型パラメータを二か所で受けるとき、推論が意図とズレることがある。
function pick<T>(items: T[], fallback: T): T {
return items[0] ?? fallback
}
pick(['a', 'b'], 'c') // T = 'a' | 'b' | 'c' (意図と違う)
NoInfer は片方の枠の推論寄与をオフにして、もう一方だけで推論させる。
function pick<T>(items: T[], fallback: NoInfer<T>): T {
return items[0] ?? fallback
}
pick(['a', 'b'], 'c') // エラー — 'c' は 'a' | 'b' に入らない
pick(['a', 'b'] as const, 'a') // OK
ライブラリシグネチャでよく使われる。ユーザーが二か所に違う型を入れたときに、曖昧な推論ではなくエラーを出したいとき。
const 型パラメータ — リテラル保持
function tuple<T extends readonly unknown[]>(...items: T): T {
return items
}
tuple(1, 'a', true) // T: (number | string | boolean)[]
const を付けると、呼び出し側が as const を書いたのと同じ効果になる。
function tuple<const T extends readonly unknown[]>(...items: T): T {
return items
}
tuple(1, 'a', true) // T: readonly [1, 'a', true]
呼び出し側の入力をキーとして使うあらゆるライブラリ(ステートマシン・ルーター・permission)で有用だ。
infer extends — infer の結果を絞る
type ToNumber<S extends string> =
S extends `${infer N extends number}` ? N : never
type N = ToNumber<'42'> // 42
type X = ToNumber<'abc'> // never
infer N extends number で N が number を満たすときだけマッチ。文字列パース・ルートパラメータ型・SQL ビルダーで強い。
採用の価値
三つともに ライブラリ作者 にもっとも有用だ。アプリケーションコードでは時々登場する。知らずに動くコードは書けるが、知ればライブラリの API が一段すっきりする。
第7章 · isolatedDeclarations — モノレポがふたたび速くなる
5.5 で入ったオプション。2026年にはモノレポの標準装備になった。
何が問題だったか
ライブラリパッケージが .d.ts を出すには、TS コンパイラが 公開 API の型をすべて推論しなければならない。これが重い。
// pkg/a/src/index.ts
import { compute } from 'somewhere'
export const result = compute('input')
// コンパイラ: result の型を知るために compute のシグネチャを辿る
// -> somewhere のグラフを全部見る必要がある
モノレポでパッケージ A のビルドがパッケージ B の型に依存すれば、B の変更が A のビルドを無効化する。
isolatedDeclarations
このオプションをオンにすると、すべての公開 export に明示的な型注釈を要求 する。
// 上と同じだが明示的な注釈
import { compute } from 'somewhere'
export const result: string = compute('input')
代償は何か? 宣言ファイルを推論なしで作れる。 esbuild・swc・oxc のようなツールが TS コンパイラなしに .d.ts を出力できる。モノレポビルドが並列化・キャッシュ化される。
採用の価値
ライブラリパッケージ・公開 SDK には採用の価値がある。一度注釈を入れる作業はあるが、ビルド時間が段階的に縮む。 モノレポの内部パッケージにも良い。アプリケーションコードに強制する必要はない。
第8章 · ランタイム検証の地形図 — Zod 4、ArkType、TypeBox
型はコンパイル時にしか生きない。外から来るデータ(API レスポンス・DB 結果・環境変数・LLM 出力)は ランタイムで 検証する必要がある。2026年の地形図。
Zod 4
もっとも広く使われる選択。2025年に v4 が出て:
- パース性能が v3 比およそ3倍。
- ツリーシェイク可能な構造でバンドルが小さく。
z.discriminatedUnionが安定化。- エラーメッセージが i18n をサポート。
import { z } from 'zod'
const User = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().int().min(0),
})
type User = z.infer<typeof User>
const parsed = User.parse(rawJson) // 無効なら throw
ArkType
構文が TS の型表現を真似た検証ライブラリ。型とスキーマを一度に書く。
import { type } from 'arktype'
const User = type({
id: 'string',
email: 'email',
age: 'number.integer >= 0',
})
type User = typeof User.infer
const out = User(rawJson)
if (out instanceof type.errors) console.error(out.summary)
else /* out is User */
ランタイムコストが非常に低い。大きなペイロードを頻繁に検証するところ(API ゲートウェイ・メッセージキューコンシューマ)で光る。
TypeBox
JSON Schema を直接出力する。AJV・OpenAPI のような JSON Schema 生態系と相性がいい。
import { Type, Static } from '@sinclair/typebox'
const User = Type.Object({
id: Type.String({ format: 'uuid' }),
email: Type.String({ format: 'email' }),
age: Type.Integer({ minimum: 0 }),
})
type User = Static<typeof User>
JSON Schema を外部に公開する API・OpenAPI 文書を自動生成するバックエンドに合う。
選び方
- Zod 4 — もっとも安全なデフォルト。生態系と資料が一番多い。
- ArkType — 性能が重要なホットパス。型表現をそのまま書きたいとき。
- TypeBox — JSON Schema が一級市民な環境。
第9章 · 何を実際に採用するか — 率直なマトリクス
機能ごとの採用価値を一行で。
| 機能 | 採用 | 補足 |
|------------------------------|-----------|--------------------------------------|
| tsc-go | 即時 | ほぼ無料の10倍。CI から点ける。 |
| `using` / `await using` | 漸進 | ライブラリから、新コードから。 |
| `Promise.withResolvers()` | 即時 | リスク0、すべての新コードで。 |
| `satisfies` | 即時 | ほぼすべての `as` を置き換え。 |
| テンプレートリテラル型 | 選別 | ルート・API・イベント名のみ。 |
| 標準デコレータ | ライブラリ| ORM・DI のような場所のみ。 |
| `NoInfer<T>` | ライブラリ| API シグネチャの精緻化に。 |
| `const` 型パラメータ | ライブラリ| リテラル保持が重要な場所。 |
| `infer extends` | 時々 | 文字列パース・ルート。 |
| `isolatedDeclarations` | ライブラリ| モノレポ・公開 SDK の標準。 |
| Zod 4 | デフォルト| 外部データ検証。 |
| ArkType | ホットパス| 性能が重要な場所。 |
| TypeBox | OpenAPI | JSON Schema 生態系。 |
エピローグ — TS 6 は二語、そして五つの習慣
ここまでの全ページを5行に圧縮すると。
- コンパイラを替えろ —
tsc-goに。最大の報酬、最小のリスク。 usingを覚えろ — リソースを扱うライブラリが間もなくそれに続く。satisfiesをデフォルトに —asは最後の手段に。Promise.withResolversに移れ — 一行の変化だが、毎日一度光る。- デコレータは場所を選んで使え — メタプログラミングの席、さもなくば関数。
チェックリスト — モノレポに一度だけ適用
- TypeScript 6 (または 5.7+) を使用
-
tsc-goを CI の一つのジョブで先に有効化 -> 診断差分を比較 -> デフォルトに切り替え -
tsconfig.jsonで"target": "es2024"、"lib": ["es2024", "dom"] - 新コードで
usingを使う — リソース抽象化ライブラリから -
asを検索 -> 可能な箇所をsatisfiesに移行 - ライブラリパッケージに
isolatedDeclarations: true -
experimentalDecorators: false— 新セマンティクスに統一 - Zod 4 に移行 (v3 は段階的終了へ)
-
tsserver-go バックエンドをエディタで有効化
アンチパターン TOP 10
anyに逃げる — ほぼ常にunknownのほうが良い。絞ってから進む。asで強制キャスト —satisfiesでできるか先に見る。try/finallyのネスト —usingができる場所。new Promiseで外部解決を作る —Promise.withResolversで一行。- デコレータ濫用 — すべてのメソッドに
@logのようなものは罠。 - 型パラメータの爆発 — 関数引数より型パラメータが多ければ見直す。
- 巨大な conditional type — コンパイルが遅くなる。分割・キャッシュ・テスト。
- 大きな設定オブジェクトを
satisfiesなし — キーのタイポが見逃される。 enumの使用 —as constオブジェクトのほうがほぼ常に良い(ツリーシェイク・型推論)。- DefinitelyTyped のバージョンずれ — ランタイムバージョンと
@types/*を同期。
次回予告
次回は Rust 1.85 と async Rust の成熟 — async fn in trait が安定したあとの地形、Tokio・async-std・smol の分岐、axum・actix・loco の比較、そして「Rust を採用するときとしないとき」の率直なガイド。同じトーン、同じ深さで一気通貫にまとめる。
「TypeScript の本当の価値は、標準になった後にゆっくり立ち現れる。2026年に私たちが見ているのは、コンパイラが書き直されてもコードはそのままだ、という事実だ。」 — TS 6 と tsc-go ポート、終わり。
参考 / References
- TypeScript 公式サイト
- TypeScript 6 リリースノート
- tsc-go GitHub リポジトリ
- Anders Hejlsberg — A 10x Faster TypeScript (2025)
- TC39 Explicit Resource Management プロポーザル
- TC39 Stage 3 デコレータプロポーザル
- TC39 Promise.withResolvers プロポーザル
- TypeScript 5.0 — デコレータ導入
- TypeScript 5.2 —
using導入 - TypeScript 5.4 —
NoInfer<T> - TypeScript 5.5 —
isolatedDeclarations - Zod 公式ドキュメント
- ArkType 公式ドキュメント
- TypeBox GitHub
- Node.js 22 —
usingサポート - V8 — Promise.withResolvers
- MDN — Symbol.dispose
- DefinitelyTyped
- tsserver-go RFC