Angular IDPStrong Junior
Модулі / Форми / Типізовані форми
🔥 0

Модуль 6 · урок 3 з 12

Типізовані форми

«Strongly typed ReactiveFormsModule» — дослівна вимога з таблиці. До версії 14 форми в Angular були фактично нетипізованими, і form.value.titel мовчки давав undefined. Тепер це помилка компіляції — але лише якщо форму описати правильно.

Тип виводиться з початкового значення

const form = new FormGroup({
  title: new FormControl('', { nonNullable: true }),
  hours: new FormControl(1, { nonNullable: true }),
});

form.value.title;    // string
form.value.hours;    // number
form.value.titel;    // ❌ помилка компіляції — такого поля немає

form.controls.title.setValue(42);   // ❌ number у поле типу string

Нічого спеціально писати не треба: TypeScript виводить тип із того, що передали початковим значенням. Саме тому важливо не писати new FormControl() без аргументів — тоді тип стане any, і вся типізація зникне.

nonNullable: навіщо це насправді

// без nonNullable
const a = new FormControl('');
a.value;      // string | null   ← звідки null?
a.reset();    // значення стає null!

// з nonNullable
const b = new FormControl('', { nonNullable: true });
b.value;      // string
b.reset();    // значення стає '' — початкове
Ось у чому річ

За замовчуванням reset() ставить контролу null, а не початкове значення. Тому тип і містить null — це не примха, а чесне відображення поведінки. nonNullable: true міняє саму поведінку: reset() повертає до початкового значення, і null зникає з типу.

Наслідок для коду: без nonNullable у кожному місці доведеться писати value ?? '', а з ним — ні. У модулі 2 ми домовились про нуль any і чесні типи; тут це те саме правило, застосоване до форм.

Явний тип форми

type TaskFormValue = {
  code: FormControl<string>;
  title: FormControl<string>;
  estimate: FormControl<number>;
  assigneeId: FormControl<string | null>;
};

// тепер тип форми можна передавати між методами й компонентами
protected readonly form: FormGroup<TaskFormValue> = this.fb.nonNullable.group({ … });

function fillFrom(form: FormGroup<TaskFormValue>, task: Task): void {
  form.patchValue({ code: task.code, title: task.title });
}

Явно описувати тип потрібно не завжди — здебільшого виведення справляється саме. Але коли форма передається в іншу функцію чи компонент, іменований тип рятує від нечитабельних сигнатур.

Звʼязок із моделлю

// utility types із модуля 2 знову в ділі
type NewTask = Omit<Task, 'id' | 'createdAt'>;

// значення форми має точно відповідати тому, що приймає API
save(): void {
  const payload: NewTask = this.form.getRawValue();   // ❌ помилка, якщо структури розійшлись
  this.api.create(payload);
}

Ось за що варто взятись свідомо: тип значення форми має збігатися з типом DTO. Тоді додане в модель поле одразу підсвітить форму, у якій його забули. Якщо ж робити this.api.create(this.form.value as NewTask), уся типізація перекреслюється одним словом — і саме такий as ESLint із модуля 2 має ловити на рев'ю.

FormRecord — коли ключі наперед невідомі

// динамічний набір полів: наприклад, значення атрибутів із довідника
const attrs = new FormRecord<FormControl<string>>({});

attrs.addControl('color', new FormControl('', { nonNullable: true }));
attrs.removeControl('color');

FormGroup має фіксований набір ключів, відомий на етапі компіляції. Коли поля приходять із конфігу — потрібен FormRecord: ключі динамічні, але тип значень однаковий і перевіряється.

Типові помилки типізації

Правильно
  • fb.nonNullable.group({ … }).
  • Початкове значення завжди вказане.
  • getRawValue() для відправки.
  • Тип значення форми збігається з DTO.
Помилки
  • new FormControl() без аргументів — тип стає any.
  • as NewTask при відправці — типізація перекреслена.
  • new FormGroup({}) із додаванням контролів пізніше — тип порожній.
  • value.field! замість nonNullable.

≈ 45 хв · +25 XP за урок