Langsung ke konten
~/rifqi_abrory

Halaman

Beranda
Tentang
Stack
Pengalaman
Proyek
Tulisan
Kontak
Semua proyek

Proyek

Avrist PINTAR — Agent Portal
Avrist e-Application
Avrist Academy — LMS CMS
Avrist e-Payment
PRUHub
SIYAP

Artikel

Angular signals untuk halaman admin sehari-hari
Typed reactive forms di Angular, tanpa any
Error API yang konsisten di ASP.NET Core dengan ProblemDetails
Meneruskan CancellationToken sampai ke lapisan terbawah di ASP.NET Core
Graceful shutdown untuk service HTTP di Go
Idempotency key: membuat API pembayaran aman untuk di-retry
Form kontak yang tahan spam dengan Next.js Server Actions
Form yang ramah offline di React Native: draft dan antrean submit
Validasi request dan penanganan error di Spring Boot 3
Keyset pagination di SQL Server: ketika OFFSET mulai lambat

Aksi

Ganti ke mode gelap
Read in English
Salin alamat email
Semua artikel

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 NonNullableFormBuilder supaya nilainya tidak tiba-tiba null.
  • Submit dengan getRawValue() untuk mendapatkan semua field dengan tipe lengkap.
  • Simpan fungsi toRequest bertipe di antara form dan kontrak API.
  • Taruh aturan multi-field di group, dan tampilkan error dengan satu helper bersama.