TT Lab
料金プラン

한국어 English 日本語

はじめる
ブログ

ブログ

TypeScript 6 と tsc-go ポート — `using`、デコレータ、`satisfies`、型ナローイング(2026年の実務ディープダイブ)

プロローグ — 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 を定義する。コンパイラは書き直された。言語はリソース管理を手に入れた。

その間に静かに腰を据えたものも少なくない。

この記事は、それらすべてを 日々書くコード の視点から見直す。どの機能が実際にコードを変えるのか、どの機能が単に格好いいだけなのか。率直に線を引く。


第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 発表のベンチマーク:

何が速くなり、何がそのままか

速くなるもの:

そのままのもの:

2026年の採用状況 (現実的に)

tsc-go は2025年にプレビュー公開され、2026年初頭に正式リリース(typescript-go または tsc --go フラグ)に入った。TypeScript 6 は両方のコンパイラを同梱で出荷する。

しばらくは二つが共存する。診断を一致させるために大きな努力が払われたが、細部の差が出るケースはある(だからこそ二つを同梱している)。大きなコードベースを移すときは、両方のコンパイラを並走させて診断を差分比較する のが標準手順になった。

実務で何が変わるか

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 ドライバ・ファイル 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

型がきれい。意図が明確。一行短い。

どこで光るか

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年には「ふつうに使う機能」になった。

何が変わったか

新セマンティクスの下では、デコレータは次の5種類に付けられる。

それぞれのデコレータは コンテキストオブジェクト を第二引数で受ける。名前・種類・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 フィールドとゲッター/セッターを自動生成することを意味する。デコレータがその両方をフックする。

ライブラリ生態系

採用の価値 — 率直な答え

必要な場所だけ使う。 メタプログラミングが似合う場所(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 が出て:

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 文書を自動生成するバックエンドに合う。

選び方


第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行に圧縮すると。

  1. コンパイラを替えろ — tsc-go に。最大の報酬、最小のリスク。
  2. using を覚えろ — リソースを扱うライブラリが間もなくそれに続く。
  3. satisfies をデフォルトに — as は最後の手段に。
  4. Promise.withResolvers に移れ — 一行の変化だが、毎日一度光る。
  5. デコレータは場所を選んで使え — メタプログラミングの席、さもなくば関数。

チェックリスト — モノレポに一度だけ適用

アンチパターン TOP 10

  1. any に逃げる — ほぼ常に unknown のほうが良い。絞ってから進む。
  2. as で強制キャスト — satisfies でできるか先に見る。
  3. try/finally のネスト — using ができる場所。
  4. new Promise で外部解決を作る — Promise.withResolvers で一行。
  5. デコレータ濫用 — すべてのメソッドに @log のようなものは罠。
  6. 型パラメータの爆発 — 関数引数より型パラメータが多ければ見直す。
  7. 巨大な conditional type — コンパイルが遅くなる。分割・キャッシュ・テスト。
  8. 大きな設定オブジェクトを satisfies なし — キーのタイポが見逃される。
  9. enum の使用 — as const オブジェクトのほうがほぼ常に良い(ツリーシェイク・型推論)。
  10. 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

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます