DEV Community

Cover image for Strictly typed Angular Reactive Forms
Dan
Dan

Posted on • Updated on

Strictly typed Angular Reactive Forms

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.
TS hint without types
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>;
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
...
Enter fullscreen mode Exit fullscreen mode

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.
TS lint hint example

Stackblitz code

Thanks for reading my first article, I hope you found it useful. Also you can support me by buying a coffee :)

Top comments (1)

Collapse
 
ngdeveloper profile image
Ng-Developer

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.