はじめに
ここで紹介する規約は Angular を動作させるために必須ではないが、Angular 全体で一貫した書き方を推奨することで、コードの可読性と保守性を高め、プロジェクト間の移動や共有を容易にする。
このガイドは Angular 固有のスタイルに焦点を当てており、TypeScript 全般の規約については対象外。また、推奨スタイルと既存のファイルスタイルが異なる場合はファイル内の一貫性を最優先すべき。異なるスタイルが混在する方が混乱を引き起こす。
命名
ファイル名に含まれる単語はハイフンで区切る
例えば UserProfile というコンポーネントのファイル名は user-profile.ts とする。
テストファイル名は spec で終える
例えば UserProfile コンポーネントのテストファイル名は user-profile.component.spec.ts とする。
ファイル名と識別子を一致させる
ファイル名は、ファイル内のコードを表現するようにする。特に TypeScript のクラスを含む場合、クラス名に対応するファイル名をつける。
例: UserProfile コンポーネント → user-profile.component.ts
1つのファイルに複数の主要な識別子が含まれる場合は、共通のテーマを示す名前を使う。もし共通テーマに収まらない場合は、ファイルを分割することを検討する。
また、helpers.ts や utils.ts のように内容が曖昧で過度に汎用的なファイル名は避ける。
コンポーネントの TS / HTML / CSS は同じ名前にする
コンポーネントは通常、TypeScript・テンプレート・スタイルの3ファイルで構成される。これらのファイルは 同じベース名を共有し、拡張子で区別する。
例: UserProfile コンポーネント →
user-profile.component.tsuser-profile.component.htmluser-profile.component.css
もし複数のスタイルファイルを持つ場合は、末尾に説明を追加して区別する。
例:
user-profile-settings.component.cssuser-profile-subscription.component.css
プロジェクト構造
すべてのアプリケーションコードは src ディレクトリに置く
Angular の UI コード(TypeScript、HTML、スタイル)はすべて src ディレクトリの中 に配置する。一方、UI に直接関係しないコード(設定ファイルやスクリプトなど)は src の外 に置く。これにより、アプリケーションのルート構造が Angular プロジェクト間で一貫し、UI コードとその他のコードの明確な分離が保たれる。
アプリケーションの起動コードは src 直下の main.ts に置く
Angular アプリケーションの 起動(ブートストラップ)コード は常に src/main.ts に配置する。これにより、他のファイルやディレクトリに分散させないことで、起動プロセスが明確になる。
関連するファイルは同じディレクトリにまとめる
Angular のコンポーネントは通常、TypeScript ファイルと、必要に応じてテンプレートやスタイルファイルを持つ。これらのファイルは、関連するもの同士を同じディレクトリにまとめて配置するのが望ましい。ユニットテストも同様で、テスト対象のコードと同じディレクトリに置くべきである。関係のないテストをまとめて tests/ ディレクトリに集めるような構成は避ける。
プロジェクトは機能単位で整理する
プロジェクト内のコードは、機能や共通のテーマごとにサブディレクトリで整理すると分かりやすい。例えば映画館サイト「MovieReel」の場合、上映スケジュールに関するコードは show-times/、チケット予約に関するコードは reserve-tickets/ といったディレクトリにまとめ、その中でさらに細かく film-calendar/ や payment-info/ といったディレクトリに整理できる。
src/
├─ movie-reel/
│ ├─ show-times/
│ │ ├─ film-calendar/
│ │ ├─ film-details/
│ ├─ reserve-tickets/
│ │ ├─ payment-info/
│ │ ├─ purchase-confirmation
逆に、ディレクトリをコードの種類(components、directives、services など)ごとに分けるのは避けるべき。また、1つのディレクトリにファイルを詰め込みすぎて読みにくくならないよう注意する。ファイル数が増えてきたら、さらにサブディレクトリを作って整理するとよい。
1つのファイルには1つの概念だけを含める
ソースファイルはできるだけ1つの概念に集中させるのが望ましい。Angular のクラスの場合は、通常、1つのファイルに 1 つのコンポーネント、ディレクティブ、またはサービスを配置する。ただし、クラスが比較的小さく、1つの概念として密接に関連している場合は、複数のコンポーネントやディレクティブを同じファイルにまとめても構わない。迷った場合は、ファイルサイズを小さく保つ方針を優先するとよい。
依存性注入
コンストラクタパラメータより inject 関数を使う
依存性注入には、従来のコンストラクタパラメータ方式より inject 関数が使用することを推奨される。利点は次の通り:
-
可読性が高い
依存関係が多いクラスでも、injectを使うと読みやすくなる -
コメントが書きやすい
注入される依存関係に対してコメントを直接付けやすい -
型推論が改善される
TypeScript の型推論がより正確に働く -
ES2022+ の
useDefineForClassFieldsと相性が良い
フィールド宣言と初期化を分ける必要がなくなる -
既存コードのリファクタリングが容易
自動ツールを使えば、既存のコンストラクタ注入コードも簡単にinjectに置き換え可能
コンポーネントとディレクティブ
コンポーネントセレクターの選定
コンポーネントのセレクターは、HTML タグとして使用されるため、命名規則に従って一貫性を保つことが重要である。Angular の推奨事項は以下の通り:
- カスタム要素名を使用する: ほとんどのコンポーネントは、独自のカスタム要素名をセレクターとして使用する
- ハイフンを含める: HTML 仕様に従い、カスタム要素名には必ずハイフンを含める
- プレフィックスを使う: プロジェクト内のすべてのカスタムコンポーネントに一貫した短いプレフィックスを付ける
-
属性セレクターはキャメルケース: 標準のネイティブ要素に対してコンポーネントを適用する場合は、属性セレクターを使い、キャメルケースで命名する(例:
[mrTooltip]) -
ngプレフィックスは禁止: Angular の内部 API と衝突するため、独自コンポーネントではngプレフィックスを使わない
これらのルールに従うことで、コンポーネントのセレクターは一貫性を保ち、他のライブラリやフレームワークとの競合も避けられる。
コンポーネントとディレクティブのメンバー命名
コンポーネントやディレクティブのクラスメンバーは、意味のある名前を付けることで可読性と保守性を高めることが重要である。特に、入力プロパティ(@Input)や出力プロパティ(@Output)は、名前の付け方に注意する必要がある。
-
入力プロパティ (
@Input) は、テンプレートから渡される値の意味を明確に表す名前にする。
例:userName、isEnabled -
出力プロパティ (
@Output) は、イベントの内容や目的を表す名前にする。動詞やchangeを含めると分かりやすい。
例:userSelected、itemsChange - その他のプロパティやメソッド も、処理内容が直感的に分かる名前を付けることで、テンプレートやクラスの利用者に意図が伝わりやすくなる
これにより、クラスのプロパティやメソッドが何を表しているのかが明確になり、テンプレートとの連携もスムーズになる。
ディレクティブのセレクター選定
ディレクティブのセレクターは、コンポーネントと同じアプリケーション固有のプレフィックスを使用することが推奨される。これにより、プロジェクト内で一貫性を保ち、他のライブラリやフレームワークとの衝突を避けられる。
属性セレクターを使う場合は、キャメルケースの属性名を使用する。例えば、アプリケーション名が MovieReel の場合、要素にツールチップを追加するディレクティブのセレクターは [mrTooltip] のように命名する。
Angular 固有のプロパティはメソッドより先にまとめる
コンポーネントやディレクティブでは、Angular 固有のプロパティをクラスの上部にまとめて定義するのが望ましい。ここでいう Angular 固有のプロパティとは、依存関係の注入、@Input、@Output、@ViewChild などのクエリを含む。
これらのプロパティをメソッドより先に定義することで、クラスのテンプレート API や依存関係がすぐに把握でき、可読性と保守性が向上する。
コンポーネントとディレクティブは表示に集中させる
コンポーネントやディレクティブ内のコードは、基本的に 画面上に表示される UI に関連する処理に集中させるべきである。UI から独立して意味を持つ処理やロジックは、別のファイルやクラスに切り出すことが推奨される。例えば、フォームのバリデーションルールやデータ変換処理などは、独立した関数やクラスとして定義するとよい。
こうすることで、コンポーネントやディレクティブはプレゼンテーションに専念でき、再利用性や保守性が高まる。
テンプレート内で複雑なロジックを避ける
Angular のテンプレートでは、JavaScript に似た式を使って 比較的単純なロジックを直接表現することができる。しかし、テンプレート内のコードが複雑になりすぎる場合は、TypeScript 側にロジックを移動するのが望ましい。通常は、計算プロパティ(computed)やメソッドを使ってテンプレートから切り出す。
「複雑」とみなす基準に明確なルールはないため、可読性や保守性を基準に判断するとよい。
テンプレート専用のクラスメンバーには protected を使う
コンポーネントクラスの public メンバーは基本的にクラスの公開 API として扱われ、依存性注入やクエリを通じてアクセス可能になる。テンプレートからのみ参照されるメンバーについては、protected アクセス修飾子を使用することが推奨される。
これにより、クラスの公開 API とテンプレート専用のメンバーを明確に区別でき、設計上の意図が分かりやすくなる。
@Component({
...,
template: `<p>{{ fullName() }}</p>`,
})
export class UserProfile {
firstName = input();
lastName = input();
// `fullName` is not part of the component's public API, but is used in the template.
protected fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}
Angular によって初期化されるプロパティには readonly を使う
コンポーネントやディレクティブで、Angular によって初期化されるプロパティには readonly 修飾子 を付けることが推奨される。これには、@Input で受け取る値やモデル、@Output、クエリ(@ViewChild や @ContentChild など)によって初期化されるプロパティが含まれる。
@Component({/* ... */})
export class UserProfile {
readonly userId = input();
readonly userSaved = output();
}
@Component({/* ... */})
export class UserProfile {
@Output() readonly userSaved = new EventEmitter<void>();
@ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList<PaymentMethod>;
}
readonly を付けることで、Angular によって設定された値を意図せず上書きしてしまうことを防ぎ、クラスの状態管理を安全に保つことができる。
NgClass や NgStyle よりも class と style を使う
テンプレートでのクラスやスタイルの指定には、NgClass や NgStyle ディレクティブよりも、class バインディングや style バインディング を使うことが推奨される。
class や style バインディングは標準 HTML 属性に近いシンプルな構文で書けるため、テンプレートの可読性が高まり、HTML に慣れた開発者でも理解しやすい。また、NgClass や NgStyle は追加のパフォーマンスコストがかかるため、軽量なバインディングの方が効率的である。
<!-- PREFER -->
<div [class.admin]="isAdmin" [class.dense]="density === 'high'">
<!-- OR -->
<div [class]="{admin: isAdmin, dense: density === 'high'}">
<!-- AVOID -->
<div [ngClass]="{admin: isAdmin, dense: density === 'high'}">
イベントハンドラーは発生したイベントではなく、行う処理に基づいて命名する
イベントハンドラーの命名は、どのイベントで呼ばれるかではなく、何をする処理か に基づくのが望ましい。こうすることで、テンプレートを読んだだけでイベントの役割が直感的に理解でき、可読性が向上する。
例えばクリックイベントに反応するボタンであれば、単に onClick と命名するのではなく、実際の処理に合わせて saveUser() や deleteItem() のように命名する。
<!-- PREFER -->
<button (click)="saveUserData()">Save</button>
<!-- AVOID -->
<button (click)="handleClick()">Save</button>
キーボードイベントでは、Angular の キーイベント修飾子 を使いながら、特定の処理名でハンドラーを定義することができる。
<textarea (keydown.control.enter)="commitNotes()" (keydown.control.space)="showSuggestions()">
また、イベント処理が特に長かったり複雑な場合、単一の意味のあるハンドラー名を付けるのが困難なこともある。このような場合は、handleKeydown のような一般的な名前を使い、その内部でイベントの詳細に応じてより具体的な処理に委譲してもよい。
@Component({/* ... */})
class RichText {
handleKeydown(event: KeyboardEvent) {
if (event.ctrlKey) {
if (event.key === 'B') {
this.activateBold();
} else if (event.key === 'I') {
this.activateItalic();
}
// ...
}
}
}
ライフサイクルメソッドはシンプルに保つ
ngOnInit などのライフサイクルフックには、長く複雑なロジックを直接書かないことが望ましい。代わりに、処理内容を表す 意味のある名前のメソッド を作成し、そのメソッドをライフサイクルフック内で呼び出す。
ライフサイクルフック名は「いつ実行されるか」を示すだけであり、内部で何をしているかを表す名前ではない。そのため、処理内容を別メソッドに分けることで、コードの可読性と保守性が向上する。
// PREFER
ngOnInit() {
this.startLogging();
this.runBackgroundTask();
}
// AVOID
ngOnInit() {
this.logger.setMode('info');
this.logger.monitorErrors();
// ...and all the rest of the code that would be unrolled from these methods.
}
ライフサイクルフックには対応するインターフェースを使う
Angular は各ライフサイクルメソッドに対応する TypeScript インターフェース を提供している。クラスにライフサイクルフックを追加する際は、これらのインターフェースを インポートして実装することで、メソッド名の誤りを防ぎ、正しいシグネチャで定義できる。
インターフェースを使うことで、コード補完や型チェックが効き、クラスが Angular のライフサイクル仕様に沿っていることを保証できる。
import {Component, OnInit} from '@angular/core';
@Component({/* ... */})
export class UserProfile implements OnInit {
// The `OnInit` interface ensures this method is named correctly.
ngOnInit() { /* ... */ }
}