この記事の要点
• TypeScript 5はECMAScriptデコレーター正式サポートとconst型パラメータを導入
• 内部アーキテクチャ刷新でパッケージサイズ58%削減・ビルド10-25%高速化
• 5.0〜5.5で推論能力・エディタ体験・モジュール解決が継続的に進化
TypeScript 5の概要
TypeScript 5は、大幅なパフォーマンス改善と多数の新機能を備えたメジャーリリースです。ビルド速度の向上、新しいECMAScriptデコレーター構文、型システムの強化、そして内部アーキテクチャの刷新による軽量化が実現されています。5.0以降、5.1、5.2、5.3、5.4、5.5と継続的にマイナーリリースが重ねられ、それぞれで推論能力、エディタ体験、モジュール解決が着実に進化しています。
背景
TypeScriptは2012年にMicrosoftからリリースされて以来、JavaScriptエコシステムのほぼ標準となりました。Node.js、フロントエンドフレームワーク、エッジランタイムのいずれでもTypeScriptが第一級でサポートされており、型による安全性と補完支援はもはや不可欠です。TypeScript 5ではこれまでの累積的な改善を一括して再構成し、Namespace importsを使った内部構造への刷新、モジュール解決戦略の再設計、そして言語仕様のモダン化が実施されました。
flowchart LR
TS4["TypeScript 4.x"] --> TS5["TypeScript 5.x"]
TS5 --> A["新デコレーター (Stage 3)"]
TS5 --> B["const 型パラメータ"]
TS5 --> C["moduleResolution: bundler"]
TS5 --> D["パフォーマンス大幅改善"]
TS5 --> E["全enum がユニオン化"]
デコレーターの正式サポート
ECMAScriptのデコレーター提案(Stage 3)に準拠した新しいデコレーター構文が利用可能になりました。従来のexperimentalDecoratorsフラグに依存した旧式デコレーターとは別の、標準仕様に沿った安定した実装です。
function logged<This, Args extends any[], Return>(
originalMethod: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
const methodName = String(context.name);
function replacement(this: This, ...args: Args): Return {
console.log(`Calling ${methodName} with`, args);
const result = originalMethod.call(this, ...args);
console.log(`${methodName} returned`, result);
return result;
}
return replacement;
}
class Calculator {
@logged
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(2, 3);
クラスデコレーターとアクセサデコレーター
function singleton<T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext
) {
let instance: InstanceType<T> | undefined;
return class extends target {
constructor(...args: any[]) {
if (instance) return instance;
super(...args);
instance = this as InstanceType<T>;
}
};
}
@singleton
class Database {
constructor(public url: string) {}
}
従来との違い:
experimentalDecoratorsフラグは不要になり、デコレーターのシグネチャが標準化されました。旧式デコレーターと新デコレーターは同一ファイルで混在させられません。
ポイント: TypeScript 5のデコレーターはTC39提案Stage 3に準拠しており、experimentalDecoratorsフラグなしで使用できます。
const型パラメータ
リテラル型をより簡単に推論できるようになりました。関数呼び出しのたびにas constを書く必要がなくなります。
// 従来
function getConfig<T extends readonly string[]>(items: T): T {
return items;
}
const config1 = getConfig(['a', 'b', 'c'] as const);
// TypeScript 5
function getConfig<const T extends readonly string[]>(items: T): T {
return items;
}
const config2 = getConfig(['a', 'b', 'c']);
// 型: readonly ['a', 'b', 'c']
実践的な利用
function defineRoutes<const T extends Record<string, { path: string; method: 'GET' | 'POST' }>>(
routes: T
): T {
return routes;
}
const routes = defineRoutes({
listUsers: { path: '/users', method: 'GET' },
createUser: { path: '/users', method: 'POST' },
});
type RouteName = keyof typeof routes; // "listUsers" | "createUser"
type UserPath = typeof routes.listUsers.path; // "/users"
実践メモ: const型パラメータを使うとリテラル型が推論され、as constを書く必要がなくなります。
パフォーマンスの改善
TypeScript 5では内部構造の最適化により大幅な高速化を実現しています。Namespace importsの導入により、tsc自身がツリーシェイキングされ、コンパイラパッケージサイズが劇的に縮小されました。
| 項目 | 改善率 |
|---|---|
| ビルド速度 | 10〜25%向上 |
| パッケージサイズ | 約46%削減 |
| メモリ使用量 | 10〜20%削減 |
| 型チェック時間 | 大規模プロジェクトで10〜20%短縮 |
その他の新機能
すべてのenumがユニオン型に
enum Color {
Red,
Green,
Blue
}
function paint(c: Color) {
if (c === Color.Red) { /* ... */ }
}
// Color型は Color.Red | Color.Green | Color.Blue として扱われる
// 計算されたメンバーも適切にユニオン化される
moduleResolution: bundler
Viteやesbuild、webpackなどのモダンなバンドラー向けのモジュール解決戦略が追加されました。Node.js互換のESMルールから開発体験に不要な制約(拡張子必須など)を取り除いた設定です。
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": false,
"resolveJsonModule": true,
"target": "ES2022",
"strict": true,
"noUncheckedIndexedAccess": true
}
}
—verbatimModuleSyntax
import typeと通常importを明確に分離し、出力されるJSに対する意図を一貫させるフラグ。
import type { User } from './types';
import { fetchUser } from './api';
using宣言(TS 5.2)
Stage 3のExplicit Resource Managementに対応。スコープ離脱時にリソースを自動解放できます。
{
using file = openFile('./data.txt');
// スコープを抜けるとfile[Symbol.dispose]()が自動呼び出し
}
実践的な型定義例
// 型安全なイベントエミッター
type EventMap = {
login: { userId: string };
logout: { userId: string; reason: string };
error: Error;
};
class Emitter<T extends Record<string, any>> {
private handlers: { [K in keyof T]?: Array<(payload: T[K]) => void> } = {};
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void): void {
(this.handlers[event] ??= []).push(handler);
}
emit<K extends keyof T>(event: K, payload: T[K]): void {
this.handlers[event]?.forEach((h) => h(payload));
}
}
const emitter = new Emitter<EventMap>();
emitter.on('login', ({ userId }) => console.log(`login: ${userId}`));
emitter.emit('login', { userId: 'u1' });
satisfies演算子の活用
type Route = { path: string; auth: boolean };
const routes = {
home: { path: '/', auth: false },
dashboard: { path: '/dashboard', auth: true },
} satisfies Record<string, Route>;
// keyは絞り込まれたリテラルのまま保持される
type RouteKey = keyof typeof routes; // "home" | "dashboard"
旧バージョンとの比較
| 機能 | TS 4.9 | TS 5.0 | TS 5.2 | TS 5.4 |
|---|---|---|---|---|
| 新デコレーター | - | GA | GA | GA |
| const型パラメータ | - | GA | GA | GA |
| moduleResolution: bundler | - | GA | GA | GA |
| using宣言 | - | - | GA | GA |
| NoInfer型ユーティリティ | - | - | - | GA |
| satisfies演算子 | GA | GA | GA | GA |
他言語・旧バージョンとの比較
| 項目 | TypeScript 5 | TypeScript 4 | Flow |
|---|---|---|---|
| エコシステム | 最大 | 大きい | 縮小傾向 |
| 推論性能 | 高速 | 普通 | 中 |
| 標準デコレーター | 対応 | 未対応 | 未対応 |
| バンドラー統合 | 簡単 | やや煩雑 | 難しい |
| IDE補完品質 | 優秀 | 良好 | 中 |
注意: TypeScript 5では旧experimentalDecoratorsとの互換性がありません。既存のデコレーターライブラリが新構文に対応しているか確認してください。
移行のポイント
target: ES2022以降を推奨- 新しいデコレーターへの段階的移行を検討(旧式との混在不可)
moduleResolution: bundlerの活用(Vite/Next.js/Remix等と相性良好)verbatimModuleSyntaxで型インポートの意図を明示
ベストプラクティス
- strict系フラグを全部有効化:
strict: trueに加えてnoUncheckedIndexedAccess、exactOptionalPropertyTypesを推奨 - 不要なany排除:
unknownを使い、型ガードで絞り込む - satisfiesで設定オブジェクトを型安全に: 型の絞り込みを失わない
- 型定義ファイルの分離:
src/types/に集約してモノレポ間で共有 - incremental / tsBuildInfoFile: 大規模プロジェクトで差分ビルドを有効化
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": "./.tsbuildinfo",
"composite": true
}
}
よくある落とし穴とトラブルシューティング
1. 新旧デコレーターの混在
旧式experimentalDecoratorsと新デコレーターは同じファイルで使えません。プロジェクト全体での方針を決めて移行しましょう。
2. moduleResolution: bundlerで型定義が見つからない
パッケージのexportsフィールドを参照するため、古いライブラリでは解決できないことがあります。"moduleResolution": "node16"へのフォールバックか、ライブラリのアップデートを検討します。
3. enumが想定より緩いユニオンに推論される
数値enumでは依然として数値ワイドニングが発生します。リテラル型ユニオンまたはas constオブジェクトを検討してください。
const Status = {
Idle: 'idle',
Loading: 'loading',
Done: 'done',
} as const;
type Status = typeof Status[keyof typeof Status];
4. tsc実行が遅い
skipLibCheck: trueを設定includeを必要ファイルに限定- プロジェクトリファレンスで分割
typescript-go(Goポート、将来的に10倍高速化予定)を試すのも選択肢
導入手順
# 1. インストール
npm install -D typescript@5
# 2. tsconfig.jsonの初期化
npx tsc --init --target ES2022 --module ESNext \
--moduleResolution bundler --strict
# 3. 型チェック
npx tsc --noEmit
# 4. Watch mode
npx tsc --noEmit --watch
CI統合例
# .github/workflows/typecheck.yml
name: typecheck
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx tsc --noEmit
パフォーマンスベンチマーク
大規模モノレポ(TypeScript内部のベンチマーク例)での概算値:
| プロジェクト | TS 4.9 | TS 5.0 | 改善 |
|---|---|---|---|
| VS Code | 100秒 | 78秒 | -22% |
| Playwright | 42秒 | 34秒 | -19% |
| TypeScript自体 | 76秒 | 66秒 | -13% |
数値は各プロジェクトの公開ベンチマーク・リリースノート記載を参考にした目安値です。
FAQ
Q1. 旧デコレーターはすぐ使えなくなりますか?
A. experimentalDecoratorsフラグ経由で引き続き利用可能です。ただし長期的には新デコレーターへの移行を推奨します。
Q2. moduleResolution: bundlerはNode.js実行時にも使えますか?
A. このオプションはあくまで「バンドラーで解決される前提」での型チェック用です。Node.js直接実行の場合はnode16またはnodenextを使います。
Q3. satisfiesと型アノテーションの違いは?
A. satisfiesは型チェックを行いつつ、リテラル型などの具体的な型情報を保持します。アノテーションは型を広げてしまうことがあります。
Q4. using宣言はどのバージョンから使えますか? A. TS 5.2で導入されました。ランタイム側でもSymbol.disposeのポリフィルが必要な場合があります。
Q5. 型チェックが遅いのですが、どうすればよいですか?
A. skipLibCheck、Project References、incrementalを活用し、巨大ユニオンや条件型のネストを避けます。
まとめ
TypeScript 5は、開発者体験の向上とパフォーマンス改善を両立したリリースです。新しいデコレーター構文、const型パラメータ、moduleResolution: bundler、using宣言、satisfiesなどにより、より型安全で表現力豊かなコードが書けるようになりました。既存プロジェクトの段階的移行を進めつつ、新規プロジェクトでは積極的にこれらの新機能を活用していきましょう。
より深い型プログラミング例
TypeScript 5の型システムは非常に強力で、実行時の値を使わずに複雑な制約を表現できます。以下はよく使うパターンの実例です。
深いオブジェクトのPath型
type Path<T> = T extends object
? {
[K in keyof T]: K extends string
? T[K] extends object
? `${K}` | `${K}.${Path<T[K]>}`
: `${K}`
: never;
}[keyof T]
: never;
type PathValue<T, P extends string> =
P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;
type User = {
id: string;
profile: {
name: string;
address: { city: string; zip: string };
};
};
type UserPaths = Path<User>;
// "id" | "profile" | "profile.name" | "profile.address"
// | "profile.address.city" | "profile.address.zip"
type CityType = PathValue<User, 'profile.address.city'>; // string
Builderパターン
type Builder<T, Required extends keyof T = keyof T> = {
[K in keyof T]-?: (value: T[K]) => Builder<T, Exclude<Required, K>>;
} & (Required extends never ? { build: () => T } : {});
type Query = {
from: string;
select: string[];
where?: string;
limit?: number;
};
declare function createQueryBuilder(): Builder<Query, 'from' | 'select'>;
const q = createQueryBuilder()
.from('users')
.select(['id', 'name'])
.where('active = true')
.limit(10)
.build();
NoInferを使った推論制御(TS 5.4)
function createFSM<State extends string>(config: {
initial: NoInfer<State>;
states: Record<State, { on: Partial<Record<string, NoInfer<State>>> }>;
}) {
return config;
}
const fsm = createFSM({
initial: 'idle',
states: {
idle: { on: { start: 'loading' } },
loading: { on: { done: 'success', fail: 'error' } },
success: { on: {} },
error: { on: { retry: 'loading' } },
},
});
ブランド型(Nominal Typing)
type Brand<T, B> = T & { readonly __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function asUserId(s: string): UserId { return s as UserId; }
function asOrderId(s: string): OrderId { return s as OrderId; }
function getUser(id: UserId) { /* ... */ }
const uid = asUserId('u_123');
const oid = asOrderId('o_456');
getUser(uid); // OK
// getUser(oid); // Error: Type 'OrderId' is not assignable to 'UserId'
プロジェクトリファレンス
大規模モノレポでは、Project Referencesを使うことで増分ビルドと依存関係の明示化ができます。
// tsconfig.json (ルート)
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true
},
"include": ["src/**/*"]
}
# 増分ビルド
tsc --build
# クリーンビルド
tsc --build --clean && tsc --build
型テスト
型自体もユニットテストで保護できます。tsdやexpect-typeを使うと、意図しない型変化を検出できます。
import { expectTypeOf } from 'expect-type';
expectTypeOf<PathValue<User, 'profile.name'>>().toEqualTypeOf<string>();
expectTypeOf<Path<User>>().toMatchTypeOf<'id' | 'profile' | 'profile.name'>();
ESLintとの統合
// .eslintrc.json
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"tsconfigRootDir": "."
},
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"rules": {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-misused-promises": "error"
}
}