1. 結論(この記事で得られること)
Angular strict mode環境でReactive Formsを型安全に扱う実装パターンを習得できます。
具体的には以下が身につきます:
- 「FormGroup」の正しい型定義とFormControlの型推論
- 「NonNullableFormBuilder」を使った安全なフォーム構築
- 「getRawValue()」と「value」の違いと使い分け
- 実務で使えるバリデーション型安全パターン
- AIを活用した型エラー解決の最短ルート
- Jasmine/Karmaでのフォームテスト戦略
私自身、Angular 14でstrict modeに移行した際、既存のフォームコードが軒並み型エラーで赤くなって途方に暮れた経験があります。この記事では、その時に確立した型安全なパターンを実コード付きで解説します。
2. 前提(環境・読者層)
対象読者
- Angular 14以降でstrict mode(「strictTemplates: true」)を有効にしている方
- Reactive Formsの基本は分かるが、型安全性に課題を感じている方
- 「as」や「!」などの型アサーションを減らしたい方
{
"@angular/core": "^17.0.0",
"@angular/forms": "^17.0.0",
"typescript": "^5.2.0"
}
tsconfig.jsonで以下が有効:
{
"strict": true,
"strictNullChecks": true,
"strictTemplates": true
}
3. Before:よくあるつまずきポイント
パターン1:型が「any」になってしまう
// ❌ 古い書き方:型情報がない
export class UserFormComponent {
userForm = new FormGroup({
name: new FormControl(''),
email: new FormControl(''),
age: new FormControl(0)
});
onSubmit() {
// ここで型がany!タイポも検出されない
const data = this.userForm.value;
console.log(data.nam); // 間違っているのに通る
}
}
このパターン、私も最初にハマりました。「value」の型が「Partial<{name, email, age}>」になり、全プロパティがundefined許容になってしまうんです。
パターン2:null/undefinedの扱いでエラー
// ❌ strictNullChecksでエラー
const nameControl = this.userForm.get('name');
nameControl.setValue('test'); // Object is possibly 'null'
パターン3:ネストしたFormGroupの型が壊れる
// ❌ 型推論が効かない
userForm = new FormGroup({
profile: new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl('')
})
});
// profile.valueの型がanyに...
const profile = this.userForm.get('profile').value;
4. After:基本的な解決パターン
解決策1:FormGroupによる厳密な型定義
import { FormControl, FormGroup, Validators } from '@angular/forms';
// ✅ フォームの型を明示的に定義
interface UserFormModel {
name: FormControl<string>;
email: FormControl<string>;
age: FormControl<number>;
}
export class UserFormComponent {
userForm: FormGroup<UserFormModel> = new FormGroup({
name: new FormControl('', { nonNullable: true, validators: Validators.required }),
email: new FormControl('', { nonNullable: true, validators: [Validators.required, Validators.email] }),
age: new FormControl(0, { nonNullable: true })
});
onSubmit() {
// ✅ 型安全!getRawValue()でnon-nullableな値を取得
const data = this.userForm.getRawValue();
console.log(data.name); // string型
console.log(data.nam); // コンパイルエラー!
}
}
重要なポイント:
- 「FormControl」で明示的に型を指定
- 「nonNullable: true」でnull/undefinedを排除
- 「getRawValue()」を使うとdisabled状態も含む完全な値を取得
解決策2:NonNullableFormBuilderの活用
import { NonNullableFormBuilder } from '@angular/forms';
import { inject } from '@angular/core';
export class UserFormComponent {
private fb = inject(NonNullableFormBuilder);
// ✅ 全コントロールが自動的にnonNullableになる
userForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
age: [0, [Validators.min(0), Validators.max(120)]]
});
onSubmit() {
if (this.userForm.invalid) return;
// ✅ 型推論が効く
const formData = this.userForm.getRawValue();
// formData: { name: string; email: string; age: number; }
}
}
これが実務では一番スッキリします。FormBuilderのショートハンド記法も使えて、コード量が減ります。
解決策3:ネストした構造の型安全化
interface AddressForm {
street: FormControl<string>;
city: FormControl<string>;
zipCode: FormControl<string>;
}
interface UserProfileForm {
firstName: FormControl<string>;
lastName: FormControl<string>;
address: FormGroup<AddressForm>;
}
export class ProfileComponent {
private fb = inject(NonNullableFormBuilder);
profileForm = this.fb.group<UserProfileForm>({
firstName: this.fb.control(''),
lastName: this.fb.control(''),
address: this.fb.group<AddressForm>({
street: this.fb.control(''),
city: this.fb.control(''),
zipCode: this.fb.control('')
})
});
// ✅ ネストした値も型安全
onSubmit() {
const profile = this.profileForm.getRawValue();
console.log(profile.address.city); // string
}
}