Typed reactive forms di Angular, tanpa any
FormGroup yang bertipe ketat, kontrol non-nullable, validasi lintas field, dan pemetaan nilai form ke request API.
2 menit baca
Di halaman ini
Form adalah tempat tipe data biasanya hilang
Aplikasi back-office penuh dengan form panjang: data nasabah, alamat, detail pembayaran. Sejak
Angular 14, reactive forms sudah bertipe ketat, tetapi banyak codebase masih membaca nilainya sebagai
any lewat form.value.someField atau form.get("someField"). Artikel ini menunjukkan pola yang
menjaga tipe dari definisi form sampai ke pemanggilan API.
Definisikan form sekali
Pakai NonNullableFormBuilder supaya reset() mengembalikan kontrol ke nilai awalnya, bukan ke
null:
import { Component, inject } from "@angular/core";
import { NonNullableFormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
@Component({
selector: "app-customer-form",
imports: [ReactiveFormsModule],
templateUrl: "./customer-form.html",
})
export class CustomerForm {
private readonly fb = inject(NonNullableFormBuilder);
readonly form = this.fb.group({
fullName: ["", [Validators.required, Validators.maxLength(100)]],
email: ["", [Validators.required, Validators.email]],
birthDate: ["", Validators.required],
address: this.fb.group({
street: ["", Validators.required],
city: ["", Validators.required],
postalCode: ["", [Validators.required, Validators.pattern(/^\d{5}$/)]],
}),
});
}TypeScript sekarang mengenali seluruh strukturnya. this.form.controls.address.controls.city adalah
FormControl<string>, dan salah ketik nama kontrol menjadi error saat compile, bukan null saat
runtime.
Baca nilai lewat getRawValue
form.value melewatkan kontrol yang disabled, sehingga tipenya menandai semua field sebagai opsional.
Saat submit, getRawValue() biasanya yang Anda butuhkan: semua field, dengan tipe lengkap.
submit() {
if (this.form.invalid) {
this.form.markAllAsTouched();
return;
}
const value = this.form.getRawValue();
// ^? { fullName: string; email: string; birthDate: string; address: { ... } }
this.api.createCustomer(toRequest(value)).subscribe();
}Petakan form ke API secara eksplisit
Bentuk form dan kontrak API jarang sama persis. Fungsi pemetaan kecil menyimpan perbedaan itu di satu tempat dan membiarkan compiler memeriksa kedua sisinya:
type CustomerFormValue = ReturnType<CustomerForm["form"]["getRawValue"]>;
export type CreateCustomerRequest = {
name: string;
email: string;
birthDate: string; // ISO yyyy-mm-dd
address: { line1: string; city: string; postalCode: string };
};
function toRequest(value: CustomerFormValue): CreateCustomerRequest {
return {
name: value.fullName.trim(),
email: value.email.trim().toLowerCase(),
birthDate: value.birthDate,
address: {
line1: value.address.street.trim(),
city: value.address.city.trim(),
postalCode: value.address.postalCode,
},
};
}Ketika backend mengganti nama field, error-nya muncul di toRequest, bukan di production.
Validasi lintas field
Sebagian aturan melibatkan lebih dari satu kontrol, misalnya rentang tanggal. Pasang validator-nya di group:
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
export const dateRange: ValidatorFn = (group: AbstractControl): ValidationErrors | null => {
const from = group.get("from")?.value as string | undefined;
const to = group.get("to")?.value as string | undefined;
return from && to && from > to ? { dateRange: true } : null;
};
readonly period = this.fb.group(
{ from: ["", Validators.required], to: ["", Validators.required] },
{ validators: dateRange },
);String tanggal ISO (yyyy-mm-dd) bisa dibandingkan dengan benar sebagai string biasa, sehingga
validator-nya bebas dari kejutan zona waktu.
Tampilkan error secara konsisten
Daripada mengulang rangkaian *ngIf untuk setiap field, tulis satu helper kecil dan pakai di mana-mana:
showError(control: AbstractControl, error: string): boolean {
return control.hasError(error) && (control.touched || control.dirty);
}<label for="email">Email</label>
<input id="email" type="email" formControlName="email" />
@if (showError(form.controls.email, "email")) {
<p class="error">Enter a valid email address.</p>
}Pasangkan pesannya dengan aria-invalid dan aria-describedby pada input supaya pembaca layar juga
mengumumkan error-nya.
Ringkasan
- Bangun form dengan
NonNullableFormBuildersupaya nilainya tidak tiba-tibanull. - Submit dengan
getRawValue()untuk mendapatkan semua field dengan tipe lengkap. - Simpan fungsi
toRequestbertipe di antara form dan kontrak API. - Taruh aturan multi-field di group, dan tampilkan error dengan satu helper bersama.