Hi! Please feel free to correct my grammar (English is not my native language) or mistakes in code. I appreciate any feedback.
The angular reactive form is a great tool to work with. But there is one issue that makes the experience not so smoother, let's talk about types. We can't use IDE autocomplete when try to access the form's controls
property. We should always remember the correct spelling of names that we give to our controls and their value types, it's very annoying.
What can we do? Once, I was searching for solutions and found a cool article by Georgi Parlakov, highly recommend you to read it. It inspired me to improve Georgi's approach.
import { AbstractControl, FormArray, FormGroup } from '@angular/forms';
/**
* Form.controls autocomplete with value types.
*/
export type FormControls<T> = {
[key in keyof T]: T[key] extends TForm<any> | FormArray // If control value has type of TForm (nested form) or FormArray
? T[key] // Use type that we define in our FormModel
: Omit<AbstractControl, 'value'> & { value: T[key] } // Or use custom AbstractControl with typed value
};
export type TForm<T> = FormGroup & {
controls: FormControls<T>;
};
Firstly, we need to create a generic type, which extends angular FormGroup and rewrites controls
property to a custom dynamic type, let's call it TForm. Then, we need to make another generic type for our controls
property (FormControls), [key in keyof T]
helps us get access to a key (name of the control) and a value (type of the control) of each property inside our generic type (ProfileFormModel). We left key
as it is, but value type depends on which type we pass into generic, if it's a nested form (Form Group or FormArray) we use it as a type of the control, otherwise let's use AbstractControl but with a few changes.
Omit<AbstactControl, 'value'> & { value: T[key] }
this construction allows us to use AbstractControl where value
property was removed and added again, but now with a type.
...
type AddressFormModel = { // <-- Nested form structure
street: number;
city: string;
state: string;
zip: string;
};
type ProfileFormModel = { // <-- Main form structure
firstName: string;
lastName: string;
address: TForm<AddressFormModel>; // Strongly typed nested form
aliases: FormArray;
};
@Component({
selector: 'app-profile-editor',
templateUrl: './profile-editor.component.html',
styleUrls: ['./profile-editor.component.css']
})
export class ProfileEditorComponent {
profileForm: TForm<ProfileFormModel> = this.fb.group({
firstName: ['', Validators.required],
lastName: [''],
address: this.fb.group({
street: [''],
city: [''],
state: [''],
zip: ['']
}),
aliases: this.fb.array([this.fb.control('')])
}) as TForm<ProfileFormModel>; // <-- Need to specify type.
get aliases() {
return this.profileForm.get('aliases') as FormArray;
}
...
Next time when we use our form controls, we'll see that TS knows our form's structure and will autocomplete along with a typed value.
Thanks for reading my first article, I hope you found it useful. Also you can support me by buying a coffee :)
Top comments (1)
Hi, you can still set value any type.
this.profileForm.controls.address.controls.street.setValue('not a number');
but you wrote street type is number.