Модуль 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.