Angular has a very powerful feature called reactive forms. As powerful as this feature is, though, reactive forms have had one significant drawback for while—the lack of strict typing. Finally, this shortcoming was improved in Angular v14.
Let’s look at this in more detail to see how it helps to improve our development.
this.form = this.fb.group({
username: ['', Validators.required],
password: ['', Validators.required],
});
// Angular 14
this.form.value // value: any
this.form.value.username // username: any
this.form.value.firstName // No errors here
// Angular 15
this.form.value.username // username: string
this.form.value.firstName // Property 'firstName' does not exist
As shown in Angular 14, we don’t have any suggestions about the available properties because the value
and its properties have a type any
. Because of this, TypeScript will not throw any errors for non-existant properties.
After the update to Angular 15, TypeScript knows about form control types and can show us suggestions or throw an error if the property is not a part of the form.
Providing Form Type as a Generic
After the update, you can finally use generic-type with form classes. So, after ng update
, all FormControl classes will be replaced with UntypedFormControl to avoid any breaks in existing code. Let’s check it on the user form.
// Before
form = new UntypedFormGroup({
email: new UntypedFormControl(null),
age: new UntypedFormControl(null)
});
// After
form = new FormGroup({
email: new FormControl<string>(''),
age: new FormControl<number|null>(null)
});
However, if you expect to put DTO directly for FormGroup
or FormArray
– it’s not quite so. Form generic expects any AbstractControl (for FormGroup) or array (for FormBuilder) for its properties.
interface IUser {
email: string;
age: number;
}
form = new FormGroup<IUser>({
email: new FormControl(''),
age: new FormControl(null)
});
In the case above, we will get a type error because the FormGroup
generic expects an AbstartControl
type for each property. TypeScript will throw an error: “Type string is not assignable to type AbstractControl.”.
The Solution
We can create a TypeScript utility type that will parse the interface properties and wrap each one with the correct FormControl type.
type ToFormGroup<T> = {
[P in keyof T]: FormControl<T[P]>;
};
export type ToFormControl<T> = T extends Array<infer ArrayType> ? FormArray<ToFormControl<ArrayType>> : T extends object ? FormGroup<ToFormGroup<T>> : FormControl<T | null>
Let me explain what is happening in the code above. The ToFormGroup type gets our model and applies ToFormControl for each property. We’re checking if a property is an array at the very beginning. Getting the type of array using infer
and returning FormArray
as a recursive call of ToFormControl
. If the value is not an array, TypeScript will check if T
is an object, and if it is, it will return FormGroup
as a recursive call of ToFormGroup
with generic T
(to go through all properties till the last nested one). And if it’s not – that means the property is primitive and FormControl
should be returned.
Putting It All Together
Here’s what it might look like to put the new generic utility type to use in a real form:
interface IUser {
firstName: string;
lastName: string;
email: string;
age: number;
}
@Component({
selector: 'app-user-profile',
templateUrl: './user-profile.component.html',
styleUrls: ['./user-profile.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserProfileComponent {
form = new FormGroup<ToFormGroup<IUser>>({
firstName: new FormControl(''),
lastName: new FormControl(''),
email: new FormControl(''),
age: new FormControl(null)
});
}
The code above will allow your IDE to help you with code suggestions and error detection. For example, if any property of IUser
interface is added/removed, becomes non-required (question mark added to property), or has a type change, your build will fail because the form controls will not be valid any more.
Conclusion
The Angular team did a great job with reactive forms, and they’ve made them even more powerful since they were first introduced. No other framework has such a feature right out of the box. However, you can see that there is still room for improvement, which makes me excited to see what valuable improvements and features are added in the future.