Angular 22 とは、2026年6月3日にリリースされた Angular のメジャーバージョンで、Signal Forms と resource の安定版化、OnPush の既定化、@Service デコレーターなどが入ったリリースです。 その後 22.1(7月)、22.2(2026年9月23日)で @boundary や Router resources などが追加されています。

この記事では、22.0〜22.2 の新機能を公式ブログ・公式ドキュメント・npm パッケージの型定義で確認して一覧表にまとめ、さらに Angular 21 のプロジェクトを実際に ng update で 22.2.0 に上げたときのメッセージと差分、新機能をコピペで試せるコードと Playwright(Chromium)での実行結果 を載せています。概要だけでなく「上げると何が変わるか」「どう書くか」まで一度に確認できます。

Angular 22 新機能の早見表(22.0〜22.2)

分類 新機能・変更 版 状態
フォーム Signal Forms(@angular/forms/signals) 22.0 安定版
非同期 resource() / httpResource() / rxResource() 22.0 安定版
非同期 resource の chain()(別の resource の結果を待つ) 22.0 安定版
非同期 debounced()(シグナルのデバウンス) 22.0 実験的
変更検知 OnPush が既定、Default は Eager に名称変更 22.0 既定の変更
DI @Service()(@Injectable({ providedIn: 'root' }) の簡易版) 22.0 安定版
DI injectAsync()(サービスの遅延読み込み) 22.0 安定版
テンプレート スプレッド構文(オブジェクト・配列・関数呼び出し) 22.0 安定版
テンプレート アロー関数 22.0 安定版
テンプレート @switch で複数の @case をまとめる、@default never; 22.0 安定版
テンプレート 要素の中にコメントを書ける 22.0 安定版
テンプレート strictTemplates が既定で有効 22.0 既定の変更
テンプレート @defer (on idle(500ms)) のタイムアウト指定 22.0 安定版
HTTP HttpClient が Fetch API を既定 に(withFetch() は非推奨) 22.0 既定の変更
ルーター canMatch に第3引数 currentSnapshot、paramsInheritanceStrategy の既定が 'always' 22.0 破壊的変更
SSR インクリメンタルハイドレーションが既定 22.0 既定の変更
a11y Angular Aria(@angular/aria) 22.0 安定版
AI WebMCP 連携(declareExperimentalWebMcpTool など)、Agent Skills 22.0 実験的
環境 TypeScript 6 必須(5.9 は非対応)、Node.js 20 は非対応 22.0 要件
ビルド Webpack 系(@angular-devkit/build-angular など)は非推奨 22.0 非推奨
シグナル linkedSignal の set オプション 22.1 安定版
DI @Injectable({ providedIn: 'root' }) → @Service() の移行コマンド 22.1 ―
HTTP JSONP(withJsonpSupport())が非推奨 22.1 非推奨
ビルド Rolldown によるチャンク最適化が安定版 22.1 安定版
サポート メジャー版のサポート期間が 2 年に 22.1 方針
テンプレート @boundary(エラー境界) 22.2 開発者プレビュー
テンプレート private メンバーをテンプレートから参照可能 22.2 安定版
ルーター Router resources(withRouterResources()) 22.2 開発者プレビュー
ルーター withAutoCleanupInjectors() 22.2 安定版
クエリ viewChild('ref', { read: Injector }) 22.2 安定版
CLI 型チェックと esbuild のバンドルを並列実行 22.2 改善
テスト Vitest 5 に更新 22.2 改善

「安定版」は本番で使ってよい API、「開発者プレビュー」は形が変わる可能性がある API、「実験的」は試用段階の API です。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular 22新機能まとめ|22.0〜22.2一覧表とng update実測」

検証環境と前提

項目 バージョン
Angular 21.2.24 → 22.2.0(ng update)、新規作成も 22.2.0
TypeScript 5.9 系 → 6.0.3
Node.js 22.22.3
テスト Vitest(新規 v22 プロジェクトは 5.0.2)
ブラウザ確認 Playwright 1.56.1 + Chromium

Angular CLI 22.2 は Node.js 22.22.3 以上 / 24.15.0 以上 / 26.0.0 以上が必要です。 22.22.2 で ng new を実行すると、次のメッセージで止まりました。

Node.js version v22.22.2 detected.
The Angular CLI requires a minimum Node.js version of v22.22.3 or v24.15.0 or v26.0.0.

上げる前に node -v を確認 しておくと、スムーズに進められます。

v21 から v22 へ ng update した実測

手順

Angular 21.2.24 で作ったプロジェクトに、変更検知・HTTP・ルーター・サービス・?. を含む小さなコードを入れてから更新しました。

npx ng update @angular/core@22 @angular/cli@22

実際に出たメッセージ

The installed Angular CLI version is outdated.
Installing a temporary Angular CLI versioned 22 to perform the update.
...
Updating package.json with dependency @angular/core to version 22.2.0...
Updating package.json with dependency typescript to version 6.0.3...
** Executing migrations of package '@angular/core' **

❯ Adds the required third argument to canMatch callsites.
  Migration completed (No changes made).

❯ Adds `ChangeDetectionStrategy.Eager` to all components.
UPDATE src/app/task-list.ts (520 bytes)
UPDATE src/app/app.ts (352 bytes)
  Migration completed (2 files modified).

❯ Adds 'withXhr' to 'provideHttpClient' function calls when the 'HttpXhrBackend' is used.
UPDATE src/app/app.config.ts (398 bytes)
  Migration completed (1 file modified).

❯ Adds withNoIncrementalHydration() opt out to provideClientHydration() when incremental hydration is not enabled to retain pre-v22 behavior-.
  Migration completed (No changes made).

❯ Migrate broken duplicate outputs.
  Migration completed (No changes made).

❯ Wraps optional chaining expressions in $safeNavigationMigration().
  Migration completed (No changes made).

❯ Disables the 'nullishCoalescingNotNullable & optionalChainNotNullable extended diagnostics.
UPDATE tsconfig.app.json (624 bytes)
  Migration completed (1 file modified).

❯ Adds 'strictTemplates: false' in tsconfig.json when not set.
  Migration completed (No changes made).

自動で入った差分

1. 全コンポーネントに ChangeDetectionStrategy.Eager が付く(動きを v21 と同じに保つため)

-import { Component, inject } from '@angular/core';
+import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
 import { TaskService } from './task.service';

 @Component({
   selector: 'app-task-list',
+  changeDetection: ChangeDetectionStrategy.Eager,
   template: `

2. HttpClient に withXhr() が付く(v22 の既定は Fetch API になったため、従来の XHR を維持)

-import { provideHttpClient } from '@angular/common/http';
+import { provideHttpClient, withXhr } from '@angular/common/http';
 ...
-  providers: [provideBrowserGlobalErrorListeners(), provideRouter(routes), provideHttpClient()],
+  providers: [
+    provideBrowserGlobalErrorListeners(),
+    provideRouter(routes),
+    provideHttpClient(withXhr()),
+  ],

3. tsconfig.app.json に拡張診断の抑制が入る

+  "angularCompilerOptions": {
+    "extendedDiagnostics": {
+      "checks": {
+        "nullishCoalescingNotNullable": "suppress",
+        "optionalChainNotNullable": "suppress"
+      }
+    }
+  }

4. package.json:@angular/* が ^22.2.0、typescript が ~6.0.3 に。vitest は ^4.0.8 のまま でした(新規作成した v22 プロジェクトは ^5.0.0)。Vitest 5 にそろえたい場合は別途更新します。

@Injectable({ providedIn: 'root' }) のサービスは ng update では変わりませんでした。@Service() への移行は、22.1 で追加された専用コマンドで行います。

npx ng generate @angular/core:service-migration
-import { Injectable, signal } from '@angular/core';
+import { signal, Service } from '@angular/core';
 ...
-@Injectable({ providedIn: 'root' })
+@Service()
 export class TaskService {

更新後のビルド・テスト

項目 v21.2.24 v22.2.0(更新後)
ng build(2回目以降、3回) 4.7 / 8.2 / 7.1 秒 4.3 / 5.6 / 2.8 秒
ng test --watch=false 1 passed 1 passed
ビルドエラー なし なし

小さなプロジェクトかつ共有環境での参考値のため、数秒のばらつきがあります。 22.2 の「型チェックとバンドルの並列化」は大きなプロジェクトほど効果が出る改善です(Ninja Squad の解説では大規模プロジェクトで約8秒の改善と紹介されています)。

更新時のチェックリスト

確認項目 対応
Node.js が 22.22.3 以上 / 24.15.0 以上 node -v で確認
TypeScript 6 で型エラーがないか ng build で確認
Eager が自動で付いた そのままで動作は同じ。新しく書くコンポーネントから OnPush+シグナル にしていく
withXhr() が付いた アップロード進捗が不要なら外して Fetch に移行できる
ルートの paramsInheritanceStrategy 既定が 'always' に。従来どおりにするなら 'emptyOnly' を明示(自動移行なし)
Webpack ビルダーを使っている ng update @angular/cli --name use-application-builder で移行を検討
Karma を使っている ng update @angular/cli --name migrate-karma-to-vitest で Vitest へ

新規プロジェクト(v22)で変わった点

ng new で作った v22 プロジェクトと ng generate の出力を確認しました。

// ng g c demo/member-card で生成(changeDetection の指定なし=OnPush)
import { Component } from '@angular/core';

@Component({
  imports: [],
  selector: 'app-member-card',
  styleUrl: './member-card.css',
  templateUrl: './member-card.html',
})
export class MemberCard {}
// ng g s demo/member で生成(@Service が既定)
import { Service } from '@angular/core';

@Service()
export class Member {}
  • コンポーネントは changeDetection を書かない=OnPush。
  • サービスは @Service() で生成 されます。
  • tsconfig.json に strictTemplates の行がありません(既定で有効 になったため)。
  • devDependencies は typescript ~6.0.2、vitest ^5.0.0、jsdom ^30.0.0 でした。

新機能をコピペで試す

以下は 1 つのコンポーネントにまとめた動作確認用コードです。Angular 22.2.0 でビルドし、Playwright で操作して結果を確認しました。

// src/app/v22/stock-store.ts
import { Service, signal } from '@angular/core';

export interface StockItem { name: string; qty: number }

@Service()
export class StockStore {
  readonly items = signal<StockItem[]>([
    { name: 'ペン', qty: 12 },
    { name: 'ノート', qty: 3 },
  ]);
}
// src/app/v22/report-exporter.ts(injectAsync で遅延読み込みするサービス)
import { Service, inject } from '@angular/core';
import { StockStore } from './stock-store';

@Service()
export default class ReportExporter {
  private readonly store = inject(StockStore); // @Service では inject() を使う
  toCsv(): string {
    return ['name,qty', ...this.store.items().map((i) => `${i.name},${i.qty}`)].join('\n');
  }
}
// src/app/v22/broken-widget.ts(@boundary の確認用に、わざと例外を投げる)
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-broken-widget',
  template: `<p class="widget">在庫: {{ qtyText() }}</p>`,
})
export class BrokenWidget {
  readonly fail = input(false);
  protected qtyText() {
    if (this.fail()) throw new Error('在庫データを読めませんでした');
    return '12個';
  }
}
// src/app/v22/v22-page.ts
import { Component, computed, inject, injectAsync, onIdle, resource, signal } from '@angular/core';
import { StockItem, StockStore } from './stock-store';
import { BrokenWidget } from './broken-widget';

type Status = 'draft' | 'pending' | 'processing' | 'done';

function fetchUser(): Promise<{ id: number; teamId: number }> {
  return new Promise((r) => setTimeout(() => r({ id: 1, teamId: 7 }), 200));
}
function fetchTeam(teamId: number): Promise<string> {
  return new Promise((r) => setTimeout(() => r(`チーム${teamId}`), 200));
}

@Component({
  selector: 'app-v22-page',
  imports: [BrokenWidget],
  template: `
    <!-- 1. スプレッド構文 -->
    <p id="spread" [class]="{ ...baseClasses, highlight: isHot() }">class spread</p>
    <p id="arr-spread">{{ [...baseTags, '新着'].join(' / ') }}</p>

    <!-- 2. アロー関数 -->
    @for (item of store.items(); track item.name) {
      <button
        class="use"
        // 要素の中にもコメントが書ける(v22)
        (click)="store.items.update(list => list.map(i => i.name === item.name ? { ...i, qty: i.qty - 1 } : i))"
      >{{ item.name }}: {{ item.qty }}</button>
    }

    <!-- 3. @switch の複数 @case と @default never -->
    @let s = status();
    @switch (s) {
      @case ('draft') { <p id="sw">下書き</p> }
      @case ('pending')
      @case ('processing') { <p id="sw">処理中</p> }
      @case ('done') { <p id="sw">完了</p> }
      @default never;
    }
    <button id="next" (click)="next()">次の状態</button>

    <!-- 4. private メンバーをテンプレートから参照(v22.2) -->
    <p id="private">{{ secretLabel }}</p>

    <!-- 5. resource の chain() -->
    <p id="chain">user={{ user.status() }} team={{ team.status() }} {{ team.hasValue() ? team.value() : '' }}</p>

    <!-- 6. injectAsync -->
    <button id="export" (click)="exportCsv()">CSV出力</button>
    <pre id="csv">{{ csv() }}</pre>

    <!-- 7. @boundary(v22.2 developer preview) -->
    <button id="break" (click)="fail.set(true)">ウィジェットを失敗させる</button>
    @boundary {
      <app-broken-widget [fail]="fail()" />
    } @error {
      <p id="fallback">表示できませんでした: {{ $error.message }}
        <button id="retry" (click)="fail.set(false); $reset()">再試行</button></p>
    }
  `,
})
export class V22Page {
  protected readonly store = inject(StockStore);
  protected readonly baseClasses = { card: true, rounded: true };
  protected readonly baseTags = ['文具', '備品'];
  protected readonly isHot = computed(() => this.store.items().some((i: StockItem) => i.qty < 3));

  private readonly order: Status[] = ['draft', 'pending', 'processing', 'done'];
  protected readonly status = signal<Status>('draft');
  next() {
    const i = this.order.indexOf(this.status());
    this.status.set(this.order[(i + 1) % this.order.length]);
  }

  private readonly secretLabel = 'private でも表示できます';

  protected readonly user = resource({ loader: () => fetchUser() });
  protected readonly team = resource({
    params: ({ chain }) => {
      const u = chain(this.user); // user が resolved になるまで team は loading のまま
      return u ? { teamId: u.teamId } : undefined;
    },
    loader: ({ params }) => fetchTeam(params.teamId),
  });

  private readonly exporter = injectAsync(() => import('./report-exporter'), { prefetch: onIdle });
  protected readonly csv = signal('');
  async exportCsv() {
    const svc = await this.exporter();
    this.csv.set(svc.toCsv());
  }

  protected readonly fail = signal(false);
}

実行結果(Playwright / Chromium)

機能 操作 画面の結果
オブジェクトのスプレッド 初期表示 class="card rounded"
配列のスプレッド 初期表示 文具 / 備品 / 新着
アロー関数 「ノート: 3」をクリック ノート: 2、在庫が3未満になり class に highlight が追加
@switch 複数 @case 「次の状態」を順に押す 下書き → 処理中 → 処理中 → 完了(pending と processing が同じ表示)
private 参照 初期表示 private でも表示できます
resource の chain() 読み込み中 user=resolved team=loading
〃 約0.25秒後 user=resolved team=resolved チーム7
injectAsync 「CSV出力」 name,qty / ペン,12 / ノート,2
@boundary 「ウィジェットを失敗させる」 表示できませんでした: 在庫データを読めませんでした(子は非表示)
@boundary の $reset() 「再試行」 在庫: 12個 に戻る

injectAsync で読み込むサービスは別チャンクになりました。 ng build の出力に次の行があります。

Lazy chunk files    | Names           |  Raw size | Estimated transfer size
chunk-BHLxGM3m.js   | report-exporter | 310 bytes |               310 bytes

@boundary で捕まえたエラーも、コンソールには ERROR Error: 在庫データを読めませんでした として記録されました。画面は代替表示に切り替わり、ログは残る ので、原因調査もしやすい動きです。

つまずいた点と直し方

1. @default never は、シグナル呼び出しを直接渡すと型が絞り込まれない

@switch (status()) と書いたところ、すべての @case を書いていても次のエラーになりました。

TS2322: Type 'Status' is not assignable to type 'never'.
  Type '"draft"' is not assignable to type 'never'.

@let s = status(); で一度変数に入れ、@switch (s) にすると通ります。 その状態で @case ('done') を消すと、期待どおり抜けを検出しました。

TS2322: Type '"done"' is not assignable to type 'never'.

2. chain() の戻り値は undefined を含む

chain(this.user).teamId と書くと TS2532: Object is possibly 'undefined'. になりました。公式の例と同じく u ? { ... } : undefined で分岐 させると通ります。

主要な新機能の解説

OnPush 既定化と Eager

v22 では changeDetection を書かないコンポーネントは OnPush になります。 OnPush は「入力が変わった・イベントが起きた・読んでいるシグナルが変わった」ときだけ再描画する方式です。シグナルで状態を持っていれば、書き方を変える必要はほとんどありません。既存コードは ng update で Eager(旧 Default)が自動で付くので、動きは変わりません(OnPush の実測は「Angular Signals使い方」の記事で紹介しています)。

@Service と injectAsync

項目 @Injectable({ providedIn: 'root' }) @Service()
役割 アプリ全体で1つのサービス 同じ(簡潔に書ける)
依存の受け取り コンストラクタ引数 / inject() inject() のみ
ルートに自動登録しない @Injectable() @Service({ autoProvided: false })
ng g s の既定 v21 まで v22 から

injectAsync は「使うときに初めて読み込む」サービス を作ります。レポート出力や重いライブラリを使う処理など、最初の画面表示に不要なサービスに向いています。prefetch: onIdle を付けるとブラウザが空いたときに先読みします。

Signal Forms の安定版化

form()・FormField([formField])・FormRoot([formRoot])・required / email / validateAsync などが v22.0 で安定版 になりました。v22 では minDate() / maxDate()、getError()、reloadValidation()、debounce(field, 'blur')、全ルールでの when オプションなどが追加されています。ログイン・会員登録フォームの完成コードは「Angular Signal Forms」の記事で紹介しています。

HttpClient の Fetch 既定化

v22 では provideHttpClient() だけで Fetch API が使われます。 withFetch() は不要になり非推奨です。Fetch ではアップロード進捗が取れないため、進捗が必要な場合は withXhr() を指定します。reportProgress は非推奨になり、reportUploadProgress / reportDownloadProgress が追加されました。

Router resources(22.2、開発者プレビュー)

ルート定義でデータ取得を宣言し、複数ルートのデータを並列に読み込む仕組みです。公式の解説に沿ったコードを示します(この記事では未実行 です)。

provideRouter(routes, withComponentInputBinding(), withRouterResources());

// ルート定義
{
  path: 'races',
  component: Races,
  resources: (context) => {
    const page = computed(() => context.queryParams()['page']);
    const races = httpResource<Array<RaceModel>>(() => `/api/races?page=${page()}`);
    return { races };
  },
}

nonBlocking() で囲むと、読み込みを待たずに画面遷移します。ガードや resource の中から throw new RedirectCommand(...) でリダイレクトできるようにもなりました。

よくある質問

Q. Angular 22 に上げると既存の画面の動きは変わりますか? A. ng update を使えば、変更検知(Eager の付与)や HttpClient(withXhr の付与)は従来どおりの動きに保たれます。 ただし paramsInheritanceStrategy の既定変更は自動移行されないため、子ルートでパラメーターを読んでいる箇所は確認しておくと安心です。

Q. Node.js と TypeScript はどのバージョンが必要ですか? A. TypeScript 6 が必須 です。Node.js は 20 が非対応になり、Angular CLI 22.2 では 22.22.3 以上 / 24.15.0 以上 / 26.0.0 以上 が必要でした(22.22.2 では起動時に停止)。

Q. @boundary は本番で使えますか? A. 22.2 時点では開発者プレビュー です。API が変わる可能性があるため、まずは影響範囲の小さいウィジェットから試すのがおすすめです。

Q. Webpack を使っているプロジェクトはどうなりますか? A. v22 では Webpack 系のビルダーは非推奨 ですが、まだ動作します。esbuild ベースの @angular/build:application へは ng update @angular/cli --name use-application-builder で移行できます。

まとめ

  • Angular 22 は Signal Forms・resource が安定版になり、OnPush が既定 になったリリースです。
  • 22.1 で linkedSignal の set・@Service 移行コマンド、22.2 で @boundary・private 参照・Router resources が加わりました。
  • ng update は Eager・withXhr・拡張診断の抑制を自動で入れ、動きを保ったまま 上げられます。
  • @Service への置き換えは ng g @angular/core:service-migration で別途実行します。
  • テンプレートで スプレッド構文・アロー関数・複数 @case・@default never が使え、コードが短くなります。
  • Node.js 22.22.3 以上と TypeScript 6 を先に用意しておくと、更新がスムーズです。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Angular 22新機能まとめ|22.0〜22.2一覧表とng update実測」

株式会社RJCでは、フレームワークの新しいバージョンを実際のプロジェクトで試し、更新の手順や気づきをメンバー同士で共有しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?