Angularのサービスとは、複数のコンポーネントで共有したいデータや処理をまとめたクラスで、依存性注入(DI)とは、そのクラスのインスタンスを自分で new せずに Angular から受け取る仕組みです。 受け取り方には inject() 関数と constructor の引数の2通りがあり、できることはほぼ同じですが、書ける場所と型の扱いに違いがあります。

この記事では、ng g s で作ったサービスを両方の方法で注入する完成コード、使える場所の比較表、「NG0201」「NG0203」「NG0200」を実際に出して原因と直し方を確かめた結果、そして providedIn: 'root' とコンポーネントの providers でインスタンスが何個作られるかのログをまとめます。

確認環境:Ubuntu 24.04 / Node.js 24.21.0 / Angular CLI 22.2.1・Angular 22.2.1 / TypeScript 6.0.3。動作は ng build と、ng test(Vitest 5.0.3 + jsdom)で確かめています。

サービスと依存性注入の早見表

やりたいこと 書き方 補足
サービスを作る ng g s cart Angular 22.2.1 では @Service() 付きのクラスができる
@Injectable で作る ng g s logger --injectable @Injectable({ providedIn: 'root' }) 付きになる
inject() で受け取る private cart = inject(Cart); フィールドの初期化かコンストラクタの中で呼ぶ
constructor で受け取る constructor(private cart: Cart) {} 引数の型がそのまま目印になる
アプリ全体で1個にする @Service() または @Injectable({ providedIn: 'root' }) 最初に注入されたときに1個だけ作られる
コンポーネントごとに1個にする @Component({ providers: [Counter] }) そのコンポーネントと子で共有、破棄も一緒
無くてもよい依存にする inject(Draft, { optional: true }) 見つからないときは null
あとから inject() を使う runInInjectionContext(injector, () => inject(Cart)) Injector を先に受け取っておく
constructor 形式を一括変換 ng generate @angular/core:inject フィールド + inject() に書き換わる

エラーコードの早見表です(詳しくは後半で再現します)。

コード 意味 主な直し方
NG0201 プロバイダーが見つからない providedIn: 'root' を付ける/providers に登録する
NG0203 注入コンテキストの外で inject() を呼んだ フィールドの初期化に移す/runInInjectionContext を使う
NG0200 サービス同士が互いを注入している 共通部分を3つ目のサービスに出す
NG2028 @Service のクラスで constructor 注入を使った inject() に変える/@Injectable にする

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

(記載例)出典:株式会社RJC「Angularサービスと依存性注入|injectとconstructorの違い」

サービスを作る:ng g s の実行結果

@Service() が既定で生成される

Angular 22.2.1 の CLI で ng g s cart を実行した結果です。

npx ng g s cart
CREATE src/app/cart.spec.ts (310 bytes)
CREATE src/app/cart.ts (75 bytes)
import { Service } from '@angular/core';

@Service()
export class Cart {}

ファイル名は cart.ts、クラス名は Cart で、デコレータは @Service() です。公式ドキュメント(Creating and using services)では、@Service は @Injectable({ providedIn: 'root' }) を短く書ける形だと説明されています。

--injectable を付けると、これまでどおりの @Injectable で生成されます。

npx ng g s logger --injectable
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class Logger {}

@Service と @Injectable の違い

公式ドキュメントの比較と、今回のビルド結果を合わせた表です。

項目 @Service() @Injectable({ providedIn: 'root' })
アプリ全体で1個(root) 何も書かなくてもそうなる providedIn: 'root' を書く
クラスの中で inject() を使う できる できる
クラス自身が constructor で依存を受け取る できない(NG2028) できる
自動で提供しない設定 @Service({ autoProvided: false }) @Injectable()(providedIn なし)
useClass などの細かな指定、providedIn: 'platform' 対象外 できる

どちらで作ったサービスも、受け取る側(コンポーネントなど)は inject() と constructor のどちらでも注入できます。この点は次の完成コードで確かめます。

inject() と constructor で注入する完成コード

備品の注文カートを例に、サービス2つ・コンポーネント3つの最小構成を作ります。

サービス:src/app/logger.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class Logger {
  readonly lines: string[] = [];

  constructor() {
    console.log('Logger を作成');
  }

  log(message: string) {
    this.lines.push(message);
    console.log('[log]', message);
  }
}

サービス:src/app/cart.ts(サービスからサービスを注入)

import { Service, computed, inject, signal } from '@angular/core';
import { Logger } from './logger';

@Service()
export class Cart {
  static created = 0;
  readonly id = ++Cart.created;

  private logger = inject(Logger);
  readonly items = signal<string[]>([]);
  readonly count = computed(() => this.items().length);

  constructor() {
    console.log(`Cart #${this.id} を作成`);
  }

  add(name: string) {
    this.items.update((list) => [...list, name]);
    this.logger.log(`Cart #${this.id} に ${name} を追加(${this.count()} 件)`);
  }
}

static created は、インスタンスが何個作られたかを数えるための目印です。

コンポーネント:src/app/add-button.ts(inject() で注入)

import { Component, inject, input } from '@angular/core';
import { Cart } from './cart';

@Component({
  selector: 'app-add-button',
  template: `<button type="button" (click)="cart.add(name())">{{ name() }} を追加</button>`,
})
export class AddButton {
  // inject() で注入(フィールドの初期化で呼ぶ)
  protected cart = inject(Cart);
  name = input.required<string>();
}

コンポーネント:src/app/cart-badge.ts(constructor で注入)

import { Component } from '@angular/core';
import { Cart } from './cart';

@Component({
  selector: 'app-cart-badge',
  template: `<span class="badge">カート: {{ cart.count() }} 件(Cart #{{ cart.id }})</span>`,
})
export class CartBadge {
  // constructor で注入(引数の型が目印になる)
  constructor(protected cart: Cart) {}
}

親コンポーネント:src/app/app.ts

import { Component } from '@angular/core';
import { AddButton } from './add-button';
import { CartBadge } from './cart-badge';

@Component({
  selector: 'app-root',
  imports: [AddButton, CartBadge],
  template: `
    <h1>備品の注文</h1>
    <app-cart-badge />
    <app-add-button name="ノート" />
    <app-add-button name="ペン" />
  `,
})
export class App {}

動きを確かめるテスト:src/app/app.spec.ts

import { TestBed } from '@angular/core/testing';
import { App } from './app';

it('inject() と constructor で同じサービスを受け取る', async () => {
  console.log('--- コンポーネントを作る前');
  const fixture = TestBed.createComponent(App);
  await fixture.whenStable();
  const el = fixture.nativeElement as HTMLElement;
  console.log('1)', el.querySelector('.badge')!.textContent);
  el.querySelectorAll('button').forEach((b) => b.click());
  await fixture.whenStable();
  console.log('2)', el.querySelector('.badge')!.textContent);
});
npx ng test --watch=false --reporters=verbose
--- コンポーネントを作る前
Logger を作成
Cart #1 を作成
1) カート: 0 件(Cart #1)
[log] Cart #1 に ノート を追加(1 件)
[log] Cart #1 に ペン を追加(2 件)
2) カート: 2 件(Cart #1)
 ✓ |demo| src/app/app.spec.ts > inject() と constructor で同じサービスを受け取る

この出力から3つのことが分かります。

  • サービスは「最初に注入されたとき」に作られる(「コンポーネントを作る前」の行より後に「を作成」が出ている)
  • Cart を注入しているコンポーネントは3つ(バッジ1つ、ボタン2つ)あるのに、「Cart #1 を作成」は1回だけ
  • inject() で受け取ったボタンと、constructor で受け取ったバッジが同じ Cart #1 を見ているので、ボタンで追加した件数がバッジに反映される

inject() と constructor の違い

比較表

観点 inject() constructor の引数
書く場所 フィールドの初期化、constructor の中、ファクトリ関数 constructor の引数だけ
クラスの外(関数)で使う できる(ルートのガード、ファクトリなど) できない
型の決まり方 戻り値から決まる。optional: true なら X | null になる 引数に書いた型がそのまま使われる
文字列やインターフェースの値を受け取る inject(TOKEN) と書く @Inject(TOKEN) を引数に付ける
継承したとき 親クラスの依存を super(...) で渡さなくてよい 子クラスが親の依存を全部受けて super(...) に渡す
フィールドの初期化で依存を使う 上から順に書けば使える TS2729 になる場合がある(後述)
@Service() のクラス自身で 使える 使えない(NG2028)
テストで手動 new する TestBed 経由で作る new Cart(fake) と直接渡せる

公式ドキュメント(Migrating to the inject function)では、inject() は constructor 注入に比べて型がより正確になり、標準のデコレータとも相性がよい、と説明されています。新しく書くコードは inject() を基本にし、既存の constructor 形式はそのまま動かしつつ少しずつそろえる、という進め方が無理のない選択です。

使える場所(注入コンテキスト)の表

inject() は「注入コンテキスト」と呼ばれる、Angular がクラスを組み立てている最中だけ使えます。公式ドキュメント(Injection context)に挙げられている場所と、今回動かして確かめた場所を合わせて表にします。

場所 inject() 今回の結果
フィールドの初期化(private cart = inject(Cart);) 使える 完成コードで動作
constructor の中(this.cart = inject(Cart);) 使える 動作を確認
useFactory や InjectionToken の factory 関数 使える 公式ドキュメントに記載
runInInjectionContext(injector, fn) の中 使える 下の「直し方」で動作
ngOnInit などのライフサイクルメソッド 使えない NG0203
クリックなどのイベント処理 使えない NG0203
コンポーネントやサービスと関係のない場所 使えない NG0203

constructor の引数は「クラスが作られるとき」にしか書けないので、場所で迷うことがありません。inject() は自由に書ける分、「クラスが作られる瞬間に呼ぶ」という決まりを意識する必要があります。

constructor 形式でつまずきやすい TS2729

constructor の引数で受け取った依存を、フィールドの初期化で使おうとすると、次のコードはビルドで止まります。

@Injectable({ providedIn: 'root' })
export class History {
  private prefix = this.logger.lines.length;
  constructor(private logger: Logger) {}
}
✘ [ERROR] TS2729: Property 'logger' is used before its initialization. [plugin angular-compiler]

    src/app/x2.ts:6:24:
      6 │   private prefix = this.logger.lines.length;
        ╵                         ~~~~~~

  'logger' is declared here.

    src/app/x2.ts:7:14:
      7 │   constructor(private logger: Logger) {}
        ╵               ~~~~~~~~~~~~~~~~~~~~~~

フィールドの初期化は、引数の代入より先に動くためです。inject() なら上から順に評価されるので、次のように書けます。

export class History {
  private logger = inject(Logger);
  private prefix = this.logger.lines.length;
}

@Service のクラスで constructor 注入を書いた場合(NG2028)

@Service() を付けたクラス自身の constructor に引数を書くと、ビルドで次のメッセージが出ました。

✘ [ERROR] NG2028: @Service class cannot use constructor dependency injection. Use the `inject` function instead. [plugin angular-compiler]

    src/app/x1.ts:5:13:
      5 │ export class History {
        ╵              ~~~~~~~

直し方は2つです。メッセージのとおり inject() に書き換えるか、constructor 形式を保ちたいなら @Injectable({ providedIn: 'root' }) に変えます(ng g s 名前 --injectable で最初からその形になります)。

NG0201:No provider を出して直す

再現コード

providedIn を書かない @Injectable() と、autoProvided: false の @Service は、どこかの providers に登録するまで注入できません。

@Injectable()
class Draft { text = '下書き'; }

@Service({ autoProvided: false })
class Scoped { text = 'scoped'; }

@Component({ selector: 'app-no-provider', template: `{{ draft.text }}` })
class NoProvider { draft = inject(Draft); }

@Component({ selector: 'app-no-provider2', template: `{{ s.text }}` })
class NoProvider2 { s = inject(Scoped); }

TestBed.createComponent(...) で作ろうとしたときのメッセージです。

A1) NG0201: No provider found for `_Draft`. Source: DynamicTestModule. Find more at https://v22.angular.dev/errors/NG0201
A2) NG0201: No provider found for `_Scoped`. Source: DynamicTestModule. Find more at https://v22.angular.dev/errors/NG0201

「Draft を作る方法(プロバイダー)が見つかりません」という意味です。クラス名の先頭の _ はビルド時に付いたもので、Source: DynamicTestModule はテスト用の環境から探したことを示しています。

直し方

目的 直し方
アプリ全体で1個にしたい @Injectable({ providedIn: 'root' }) にする(または @Service())
このコンポーネントの中だけで使いたい @Component({ providers: [Draft] }) に登録する
アプリ起動時にまとめて登録したい app.config.ts の providers に入れる
無くても動くようにしたい inject(Draft, { optional: true }) にして null を扱う

providers に登録した場合と、optional: true にした場合を動かした結果です。

@Component({ selector: 'app-with-provider', template: `{{ draft.text }} / {{ s.text }}`, providers: [Draft, Scoped] })
class WithProvider { draft = inject(Draft); s = inject(Scoped); }

@Component({ selector: 'app-optional', template: `{{ draft?.text ?? 'なし' }}` })
class OptionalUse { draft = inject(Draft, { optional: true }); }
A3) 下書き / scoped
A4) なし

ビルド時に分かる関連エラー

constructor 形式では、型から「何を注入するか」が決められないとビルドで知らせてくれます。インターフェースや string を引数の型にした場合です。

✘ [ERROR] NG2003: No suitable injection token for parameter 'clock' of class 'X'. Find more at https://next.angular.dev/errors/NG2003
  Consider using the @Inject decorator to specify an injection token. [plugin angular-compiler]

    src/app/x3.ts:5:22:
      5 │   constructor(private clock: Clock) {}
        ╵                       ~~~~~

  This type does not have a value, so it cannot be used as injection token.

インターフェースは実行時には存在しないので、目印(トークン)になれません。InjectionToken を作って inject(CLOCK) で受け取ります。

また、デコレータの無いクラスを providers に入れ、そのクラスが constructor で依存を受け取っていると、次のメッセージになりました。

✘ [ERROR] NG2005: The class 'Plain' cannot be created via dependency injection, as it does not have an Angular decorator. This will result in an error at runtime.

Either add the @Injectable() decorator to 'Plain', or configure a different provider (such as a provider with 'useFactory').

サービスにするクラスには、@Service() か @Injectable() を付けておきましょう。

NG0203:inject() を呼ぶ場所を直す

再現コード

ngOnInit の中と、クリック処理の中で inject() を呼びます。

@Component({ selector: 'app-late', template: `` })
class LateInject implements OnInit {
  ngOnInit() {
    const cart = inject(Cart);
    cart.add('ノート');
  }
}

@Component({ selector: 'app-click', template: `<button (click)="onClick()">追加</button>` })
class ClickInject {
  onClick() {
    try { inject(Cart).add('ペン'); } catch (e) { console.log('B2)', (e as Error).message); }
  }
}
B1) NG0203: The `_Cart` token injection failed. `inject()` function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://v22.angular.dev/errors/NG0203
B2) NG0203: The `_Cart` token injection failed. `inject()` function must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`. Find more at https://v22.angular.dev/errors/NG0203

メッセージには、使える場所(constructor、ファクトリ関数、フィールドの初期化、runInInjectionContext に渡した関数)がそのまま書かれています。ビルドは通り、実行したときに分かるエラーです。

直し方

基本は「フィールドで受け取っておき、あとで使う」です。どうしても後から注入したい場合は、Injector を先に受け取っておき、runInInjectionContext を使います。

@Component({ selector: 'app-fixed', template: `<button (click)="onClick()">追加</button>` })
class FixedInject implements OnInit {
  private cart = inject(Cart);          // 直し方1: フィールドで受け取っておく
  private injector = inject(Injector);  // 直し方2: Injector を受け取っておく
  ngOnInit() { this.cart.add('ノート'); }
  onClick() {
    runInInjectionContext(this.injector, () => inject(Cart).add('ペン'));
  }
}

コンポーネントを作ってボタンを押した結果です。

Logger を作成
Cart #1 を作成
[log] Cart #1 に ノート を追加(1 件)
[log] Cart #1 に ペン を追加(2 件)
状況 向いている直し方
ライフサイクルメソッドやイベント処理で使いたい フィールドで inject() しておき、this.cart を使う
共通の関数の中で inject() を使っている その関数を、フィールドの初期化か constructor から呼ぶ
条件によって後から注入したい inject(Injector) を持っておき runInInjectionContext
自作の関数が注入コンテキスト前提だと明示したい 関数の先頭で assertInInjectionContext(関数名) を呼ぶ(公式ドキュメントに記載)

NG0200:循環依存を出して直す

再現コード

注文サービスが在庫サービスを、在庫サービスが注文サービスを注入しています。

@Injectable({ providedIn: 'root' })
class OrderService {
  private stock = inject(StockService);
  total() { return this.stock.reserved() + 1; }
}
@Injectable({ providedIn: 'root' })
class StockService {
  private order = inject(OrderService);
  reserved() { return 1; }
}

TestBed.inject(OrderService) で取り出そうとした結果です。

C1) NG0200: Circular dependency detected for `_OrderService`. Source: DynamicTestModule. Path: _OrderService -> _StockService -> _OrderService. Find more at https://v22.angular.dev/errors/NG0200

Path: に、どの順で注入が一周したかが表示されます。OrderService を作るには StockService が必要で、StockService を作るには OrderService が必要、という輪になっているため、どちらも作り終えることができません。

直し方:共通部分を3つ目のサービスに出す

両方が必要としているデータや処理を、別のサービスに切り出します。

@Injectable({ providedIn: 'root' })
class ReservationStore { readonly reserved = signal(1); }

@Injectable({ providedIn: 'root' })
class OrderService2 {
  private store = inject(ReservationStore);
  total() { return this.store.reserved() + 1; }
}
@Injectable({ providedIn: 'root' })
class StockService2 {
  private store = inject(ReservationStore);
  reserved() { return this.store.reserved(); }
}
C2) 2 1

依存の向きが「OrderService2 → ReservationStore ← StockService2」と一方向になり、輪がなくなりました。

原因のパターン 直し方
2つのサービスが互いのデータを読む データを持つサービスを別に作り、両方からそれを注入する
片方がもう片方に「通知」したいだけ 通知する側がシグナルやイベントを公開し、受ける側だけが注入する
1つのサービスに役割が集まりすぎている 役割ごとに分け、依存が一方向になるよう並べ直す

Path: に表示された名前を紙に書き出し、矢印の向きを1本だけ逆にできないか考えると、切り出すべき部分が見つかりやすくなります。

providedIn: root と providers でインスタンスは何個できるか

確かめるコード:src/app/scope.spec.ts

作成と破棄のたびにログを出す Counter サービスを、3通りの方法で注入します。

import { Component, Injectable, OnDestroy, inject, signal } from '@angular/core';
import { TestBed } from '@angular/core/testing';

@Injectable({ providedIn: 'root' })
class Counter implements OnDestroy {
  static created = 0;
  readonly id = ++Counter.created;
  readonly value = signal(0);
  constructor() { console.log(`  Counter #${this.id} を作成`); }
  ngOnDestroy() { console.log(`  Counter #${this.id} を破棄`); }
}

// (1) providers なし → root の1個を共有
@Component({ selector: 'app-shared', template: `<button (click)="c.value.update(plusOne)">#{{ c.id }}: {{ c.value() }}</button>` })
class SharedPanel { c = inject(Counter); plusOne = (n: number) => n + 1; }

// (2) コンポーネントの providers → コンポーネント1個につき1個
@Component({ selector: 'app-own', providers: [Counter], template: `<button (click)="c.value.update(plusOne)">#{{ c.id }}: {{ c.value() }}</button>` })
class OwnPanel { c = inject(Counter); plusOne = (n: number) => n + 1; }

@Component({ selector: 'app-child', template: `子が見ている Counter: #{{ c.id }}` })
class Child { c = inject(Counter); }

@Component({ selector: 'app-own2', providers: [Counter], imports: [Child], template: `#{{ c.id }} <app-child />` })
class OwnWithChild { c = inject(Counter); }

@Component({ selector: 'app-host-shared', imports: [SharedPanel], template: `<app-shared /><app-shared /><app-shared />` })
class HostShared {}

@Component({ selector: 'app-host-own', imports: [OwnPanel], template: `<app-own /><app-own /><app-own />` })
class HostOwn {}

@Component({ selector: 'app-host-tree', imports: [OwnWithChild, Child], template: `<app-own2 /> | 外側: <app-child />` })
class HostTree {}

const texts = (el: HTMLElement) => Array.from(el.querySelectorAll('button')).map((b) => b.textContent);

it('インスタンスの数を数える', async () => {
  console.log('(1) providedIn: root を3か所で注入');
  const a = TestBed.createComponent(HostShared); await a.whenStable();
  (a.nativeElement.querySelector('button') as HTMLButtonElement).click(); await a.whenStable();
  console.log('  表示:', texts(a.nativeElement));

  console.log('(2) コンポーネントの providers に書いて3つ並べる');
  const b = TestBed.createComponent(HostOwn); await b.whenStable();
  (b.nativeElement.querySelector('button') as HTMLButtonElement).click(); await b.whenStable();
  console.log('  表示:', texts(b.nativeElement));

  console.log('(3) providers を書いたコンポーネントの子と、外側');
  const c = TestBed.createComponent(HostTree); await c.whenStable();
  console.log('  表示:', c.nativeElement.textContent);

  console.log('(4) コンポーネントを破棄');
  b.destroy(); c.destroy(); a.destroy();
  console.log('(5) 作成された合計:', Counter.created);
});

実行結果

(1) providedIn: root を3か所で注入
  Counter #1 を作成
  表示: [ '#1: 1', '#1: 1', '#1: 1' ]
(2) コンポーネントの providers に書いて3つ並べる
  Counter #2 を作成
  Counter #3 を作成
  Counter #4 を作成
  表示: [ '#2: 1', '#3: 0', '#4: 0' ]
(3) providers を書いたコンポーネントの子と、外側
  Counter #5 を作成
  表示: #5 子が見ている Counter: #5 | 外側: 子が見ている Counter: #1
(4) コンポーネントを破棄
  Counter #2 を破棄
  Counter #3 を破棄
  Counter #4 を破棄
  Counter #5 を破棄
(5) 作成された合計: 5
  Counter #1 を破棄

最後の「Counter #1 を破棄」は、テストが終わってアプリ全体が片付けられるときに出た行です。

結果の読み方

登録のしかた 作られる数 共有される範囲 破棄されるとき
providedIn: 'root'(または @Service()) アプリ全体で1個 どこで注入しても同じ(3つとも #1: 1) アプリが終わるとき
コンポーネントの providers: [Counter] コンポーネント1個につき1個(3つ並べると3個) そのコンポーネントと、その子 そのコンポーネントが破棄されるとき

(3) の結果も大切です。providers を書いたコンポーネントの子は親と同じ #5 を受け取り、その外側にある同じ子コンポーネントは root の #1 を受け取りました。Angular は、注入を求めたコンポーネントから親へ向かって順にプロバイダーを探し、見つからなければ root までさかのぼる、という動きをしています。

どちらに登録するかの判断表

状況 選ぶ登録先
ログイン中の会員情報、設定、カートなど、アプリ全体で1つの状態 providedIn: 'root' / @Service()
画面に同じ部品を複数並べ、それぞれ別の状態を持たせたい(入力フォーム、絞り込み条件など) コンポーネントの providers
画面を閉じたら状態も片付けたい コンポーネントの providers(ngOnDestroy が呼ばれる)
迷ったとき まず root。共有されて困る状態が出てきたら providers に移す

「root のサービスを providers にも書く」と、意図せず別のインスタンスができる点に注意してください。「追加したはずのデータが別の画面に出ない」ときは、providers に同じサービスを書いていないかを確認すると原因が見つかることがあります。

constructor 形式を inject() に一括変換する

Angular には、constructor 注入を inject() に書き換える移行コマンドがあります。constructor 形式のサービスを1つ追加して実行しました。

// 変換前:src/app/order-history.ts
import { Injectable } from '@angular/core';
import { Cart } from './cart';
import { Logger } from './logger';

@Injectable({ providedIn: 'root' })
export class OrderHistory {
  private saved: string[][] = [];

  constructor(
    private cart: Cart,
    private readonly logger: Logger,
  ) {
    this.logger.log('OrderHistory を作成');
  }

  save() {
    this.saved.push(this.cart.items());
    this.logger.log(`${this.saved.length} 回目の注文を保存`);
  }
}
npx ng generate @angular/core:inject --defaults
UPDATE src/app/cart-badge.ts (290 bytes)
UPDATE src/app/order-history.ts (491 bytes)

変換差分です。

-import { Injectable } from '@angular/core';
+import { Injectable, inject } from '@angular/core';
 import { Cart } from './cart';
 import { Logger } from './logger';

 @Injectable({ providedIn: 'root' })
 export class OrderHistory {
+  private cart = inject(Cart);
+  private readonly logger = inject(Logger);
+
   private saved: string[][] = [];

-  constructor(
-    private cart: Cart,
-    private readonly logger: Logger,
-  ) {
+  constructor() {
     this.logger.log('OrderHistory を作成');
   }
 export class CartBadge {
-  // constructor で注入(引数の型が目印になる)
-  constructor(protected cart: Cart) {}
+  protected cart = inject(Cart);
 }

private / protected / readonly はそのまま引き継がれ、中身が空になった constructor は取り除かれました(constructor に付けていたコメントも一緒に消えています)。変換後も ng build は通りました。

公式ドキュメント(Migrating to the inject function)に載っているオプションです。

オプション 役割
path 変換する範囲のフォルダ
migrateAbstractClasses 抽象クラスも変換する(既定は無効。手直しが必要になる場合がある)
backwardsCompatibleConstructors 継承先のコンパイルエラーを避けるため、constructor の引数の形を残す
nonNullableOptional @Optional 付きの引数の型を、これまでと同じ(null を含まない)扱いにする

チェックリスト

  • [ ] サービスに @Service() か @Injectable({ providedIn: 'root' }) を付けた
  • [ ] inject() はフィールドの初期化か constructor の中で呼んでいる
  • [ ] ngOnInit やイベント処理では、受け取っておいたフィールド(this.cart)を使っている
  • [ ] @Service() のクラス自身は constructor の引数で依存を受け取っていない
  • [ ] サービス同士が互いを注入していない(輪になっていない)
  • [ ] root のサービスを、意図せずコンポーネントの providers にも書いていない
  • [ ] コンポーネントごとに分けたい状態だけ providers に登録している

よくある質問

Q. inject() と constructor はどちらを使えばよいですか? A. 新しく書くコードは inject() が扱いやすい選択です。フィールドの初期化で依存を使える、継承時に super(...) へ渡さなくてよい、optional: true で型に null が入る、といった利点があります。また Angular 22.2.1 の ng g s が生成する @Service() のクラスは inject() が前提です。constructor 形式も引き続き動くので、既存コードは ng generate @angular/core:inject で少しずつそろえられます。

Q. providedIn: 'root' と @Service() は何が違いますか? A. どちらもアプリ全体で1個のインスタンスになります。@Service() は何も書かなくても root に提供される短い書き方で、クラス自身の constructor 注入や useClass などの細かな指定は対象外です。それらが必要なときは @Injectable を使います。

Q. サービスはいつ作られますか? A. 最初に注入されたときです。今回のログでも、「コンポーネントを作る前」の行のあと、コンポーネントが注入を求めた時点で「Logger を作成」「Cart #1 を作成」が出ました。どこからも注入されないサービスは作られません。

Q. テストでサービスを差し替えるにはどうしますか? A. TestBed.configureTestingModule({ providers: [{ provide: Cart, useValue: 代わりのオブジェクト }] }) のように登録すると、inject() でも constructor でも、その代わりの値が注入されます。完成コードの App で id: 99 の代わりのカートを登録したところ、バッジは「カート: 2 件(Cart #99)」と表示されました。受け取り方を変えても差し替えの書き方は共通です。

まとめ

  • サービスは共有したいデータと処理を置くクラス、DI はそのインスタンスを Angular から受け取る仕組み
  • Angular 22.2.1 の ng g s は @Service() を生成する。--injectable で @Injectable({ providedIn: 'root' }) になる
  • inject() はフィールドの初期化と constructor の中で呼ぶ。ngOnInit やクリック処理で呼ぶと NG0203
  • NG0201 は「提供されていない」。providedIn: 'root' を付けるか、providers に登録する
  • NG0200 は「互いに注入している」。Path: を読み、共通部分を3つ目のサービスに出す
  • root は全体で1個、コンポーネントの providers はコンポーネントごとに1個。子は親のものを受け取り、破棄も親と一緒
  • constructor 形式は ng generate @angular/core:inject でまとめて inject() に書き換えられる

参考・出典

  • Angular 公式ドキュメント「Creating and using services」 https://angular.dev/guide/di/creating-and-using-services
  • Angular 公式ドキュメント「Injection context」 https://angular.dev/guide/di/dependency-injection-context
  • Angular 公式ドキュメント「Migrating to the inject function」 https://angular.dev/reference/migrations/inject-function
  • Angular API リファレンス「Service」 https://angular.dev/api/core/Service

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

(記載例)出典:株式会社RJC「Angularサービスと依存性注入|injectとconstructorの違い」

株式会社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で一緒に開発しながら、 成長を楽しみませんか?