▪️

Angularで抽象クラスにフォーム定義を分離する型安全設計

に公開
2026/01/08

Angularのリアクティブフォームに型を加えたTyped Formsを解説します。

ディレクトリ構成

フォーム定義を抽象クラスに切り出すことで、コンポーネントの責務を明確に分離しています。

profile-create/
├── profile-create.component.ts       # コンポーネント
├── profile-create.component.html     # テンプレート
└── profile-create.form.abstract.ts   # フォーム定義(抽象クラス)

抽象クラスでフォーム定義

抽象クラスを使うことで、フォームの定義とコンポーネントのロジックを分離できます。
フォームの構造とバリデーション、テンプレートで使うゲッターを抽象クラスにまとめます。

import { inject } from '@angular/core';
import { FormControl, FormGroup, NonNullableFormBuilder, Validators } from '@angular/forms';

export type ProfileFormGroup = FormGroup<{
  name: FormControl<string>;
  email: FormControl<string>;
}>;

export abstract class ProfileCreateFormAbstract {
  private fb = inject(NonNullableFormBuilder);

  readonly form: ProfileFormGroup = this.fb.group({
    name: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
  });

  get nameControl(): FormControl<string> {
    return this.form.controls.name;
  }

  get emailControl(): FormControl<string> {
    return this.form.controls.email;
  }
}

https://typescriptbook.jp/reference/object-oriented/class/abstract-class

Typed Forms

Typed Formsとは、リアクティブフォームを型安全に扱うための仕組みです。
Angular 14から導入され、FormGroup と FormControl に型を付けて定義できます。

// FormGroup を型付けして、フォームの構造を固定する
export type ProfileFormGroup = FormGroup<{
  name: FormControl<string>;
  email: FormControl<string>;
}>;

readonly form: ProfileFormGroup = this.fb.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
});

これにより、存在しないフィールドへのアクセスがコンパイルエラーになります。

// 型あり:コンパイル時にエラーになる
form.controls.nmae; // Property 'nmae' does not exist
form.controls.name; // OK

NonNullableFormBuilder

NonNullableFormBuilder を使うと、フォームコントロールの値が null にならないことを保証できます。通常の FormBuilder では control.value が string | null 型になりますが、NonNullableFormBuilder では string 型になり、null チェックが不要になります。

// NonNullableFormBuilder を使用
private fb = inject(NonNullableFormBuilder);
readonly form: ProfileFormGroup = this.fb.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
});
// → FormControl<string> (null にならない)

// 通常の FormBuilder を使用した場合
import { FormBuilder } from '@angular/forms';
private fb = inject(FormBuilder);
readonly form = this.fb.group({
  name: ['', Validators.required],
  email: ['', [Validators.required, Validators.email]],
});
// → FormControl<string | null> (null の可能性がある)

null を許容しないことで、フォームの値を扱う際に null チェックが不要になります。

// NonNullableFormBuilder の場合
const name = this.form.controls.name.value;
console.log(name.toUpperCase()); // OK(string 型)

// FormBuilder の場合
const name = this.form.controls.name.value;
console.log(name.toUpperCase()); // エラー(string | null 型)
console.log(name?.toUpperCase()); // null チェックが必要

ゲッターでコントロールを公開

nameControl、emailControl のゲッターを定義することで、
HTML テンプレートから簡潔にアクセスできます。

get nameControl(): FormControl<string> {
  return this.form.controls.name;
}

form.controls.name ではなく nameControl と書けます。

https://typescriptbook.jp/reference/advanced-topics/getters-and-setters

コンポーネントでフォーム定義の抽象クラスを継承

コンポーネントは抽象クラスを継承し、画面固有のロジックだけを持ちます。
フォームの構造やバリデーションは抽象クラス側に集約されています。

import { Component } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { ProfileCreateFormAbstract } from './profile-create.form.abstract';

@Component({
  selector: 'app-profile-create',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './profile-create.component.html',
  styleUrl: './profile-create.component.css',
})
export class ProfileCreateComponent extends ProfileCreateFormAbstract {
  onSubmit(): void {
    if (this.form.invalid) {
      this.form.markAllAsTouched();
      return;
    }

    console.log('Form submitted:', this.form.getRawValue());
  }
}

HTMLテンプレートで FormControl を紐付ける

[formControl] ディレクティブで input 要素と FormControl を接続します。
これにより、入力値の変更やバリデーションエラーが自動的に FormControl に反映されます。

<form (ngSubmit)="onSubmit()">
  <input [formControl]="nameControl" />
  @if (nameControl.touched && nameControl.hasError('required')) {
  <div class="error">Name is required</div>
  }

  <input [formControl]="emailControl" />
  @if (emailControl.touched && emailControl.hasError('required')) {
  <div class="error">Email is required</div>
  } @if (emailControl.touched && emailControl.hasError('email')) {
  <div class="error">Please enter a valid email address</div>
  }

  <button type="submit">Create Profile</button>
</form>

この設計が向いているケース

本記事で紹介した「抽象クラス + Typed Forms」の構成は、
すべてのフォームに必須というわけではありません。

特に、以下のようなケースで効果を発揮します。

  • 入力項目が多く、フォーム定義が肥大化しやすい場合
  • フォームのロジック(バリデーションや初期値)を明確に分離したい場合

一方で、項目数が少ないシンプルなフォームでは、
コンポーネント内に直接定義した方が読みやすい場合もあります。

まとめ

Typed Formsによってフォームを型安全に扱える方法を解説しました。

Discussion