,

Getting Started With Angular Strictly Typed Reactive Forms

Jul 09, 2022 reading time 9 minutes

Getting Started With Angular Strictly Typed Reactive Forms

With Angular 14 the Angular Team provided a new very demanded feature: Strictly Typed Reactive Forms. That solves a lot of problems when interacting with Angular Forms, as we have the type of the model we are representing with the form now, instead of an any “type” we had before. But let’s have a look at this step by step.

With Angular 14 the Angular Team provided a new very demanded feature: Strictly Typed Reactive Forms. That solves a lot of problems when interacting with Angular Forms, as we have the type of the model we are representing with the form now, instead of an any “type” we had before. But let’s have a look at this step by step.

The Problem

Let us take a look at this FormGroup:

@Component(/* ... */)
export class FormSimpleGroupComponent implements OnInit {
  myForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      firstName: '',
      lastName: '',
      age: 0,
    });
  }

  onSubmit() {
    const formValue = this.myForm.value;
    // >>>>>>> `formValue` has the any type here!!!! <<<<<
    console.log(formValue);
  }
}

We are creating a FormGroup which displays a firstName, a lastName and an age value. The type of firstName and lastName surely is a string, and the age would have the best fit with a number. Although we know the type here in this very basic example we have no control over the type of the value, the this.myForm.value provides us.

This also provides us the possibility to use wrong properties, something like

const title = this.myForm.value.firstName.title;

Which is wrong, because the title form does not exist on the firstName.

To solve this issue, Angular provided us types in Reactive Forms.

The automatic migration

If you upgrade to Angular 14 a migration will be performed automatically for you. But Angular will not migrate to the typed versions right away. What will be done is an untyped version of your forms explicitly.

If we refer to the sample above, you can see the migration as follows:

@Component(/* ... */)
export class FormSimpleGroupComponent implements OnInit {
  myForm: UntypedFormGroup;

  constructor(private formBuilder: UntypedFormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      firstName: '',
      lastName: '',
      age: 0,
    });
  }

  onSubmit() {
    const formValue = this.myForm.value;
    // >>>>>>> `formValue` has the any type here!!!! <<<<<
    console.log(formValue);
  }
}

So what happened is the FormGroup got converted to an UntypedFormGroup and the FormBuilder converted to an UntypedFormBuilder.

Further, a FormControl will be an UntypedFormControl and a FormArray will become an UntypedFormArray.

PR Angular Forms to Untyped Forms

The manual migration to typed reactive forms

The first thing to do is manually revert the types back from UntypedFormBuilder to FormBuilderUntypedFormGroup to FormGroup etc.

You can do this step by step, as the untyped forms still work the same way you the old ones did before Angular 14.

@Component(/* ... */)
export class FormSimpleGroupComponent implements OnInit {
  myForm: FormGroup;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      firstName: '',
      lastName: '',
      age: 0,
    });
  }

  onSubmit() {
    const formValue = this.myForm.value;
    console.log(formValue);
  }
}

But with this, you’re not completely typed yet. What Angular does now is deriving the form types from your given controls.

If you hover over the this.formBuilder.group(...) VSCode will provide you the types Angular “guesses” for you. You will see something like this:

FormBuilder.group<{
    firstName: string;
    lastName: string;
    age: number;
}>(controls: {
    firstName: string;
    lastName: string;
    age: number;
}, options?: AbstractControlOptions): FormGroup<{
    firstName: FormControl<...>;
    lastName: FormControl<...>;
    age: FormControl<...>;
}>

So Angular knows what types are in the form now, but that’s only a small step on the way to typed forms. We are not there yet.

If we want to define the type of our forms we can add it to the FormGroup type our property has like so:

@Component(/* ... */)
export class FormSimpleGroupComponent implements OnInit {
  myForm: FormGroup<{
    firstName: FormControl<string>;
    lastName: FormControl<string>;
    age: FormControl<number>;
  }>;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      firstName: '',
      lastName: '',
      age: 0,
    });
  }

  onSubmit() {
    const formValue = this.myForm.value;
    console.log(formValue);
  }
}

And suddenly Angular can now check if the controls inside your form

this.myForm = this.formBuilder.group({
  firstName: '',
  lastName: '',
  age: 0,
});

Really match your FormGroup type.

If we provided a control name which is not in the type, the form would set an error.

 myForm: FormGroup<{
    firstName: FormControl<string>;
    lastName: FormControl<string>;
    age: FormControl<number>;
  }>;

// ...

this.myForm = this.formBuilder.group({
      firstName: "",
      lastName: "",
      somethingElseThanAge: 0,  <<<<<<< This would error
    });

Now, if you want to get the forms value including all fields, which means also the not enabled controls, you can get the value with the getRawValue() method, which also has the types now and provides intellisense. You can also use the value property provided by the form group, which provides the value as well but does not contain possibly not enabled controls or undefined fields.

 onSubmit() {
    const rawValue = this.myForm.getRawValue();
    // or
    const value = this.myForm.value;

    const notExisting = value.age; // this works fine
    const notExisting = value.notOnMyForm  <<<<<<< This would error
  }

Also, if you are working with this.myForm.setValue(...) or this.myForm.patchValue(...) the values passed into the methods are now being checked and if not fitting, the compiler throws an error again to safe you from providing wrong values to your form.

But having the type of the form directly in the generic property description is a little cumbersome to read. We can make our lives easier and introduce an interface for this and provide this as the type into the generic of the FormGroup.

interface UserForm {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
  age: FormControl<number>;
}

@Component(/* ... */)
export class FormSimpleGroupComponent implements OnInit {
  myForm: FormGroup<UserForm>;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      firstName: '',
      lastName: '',
      age: 0,
    });
  }

  onSubmit() {
    const formValue = this.myForm.value;
    console.log(formValue);
  }
}

All the things work like before, but the myForm: FormGroup<UserForm>; is a little easier to read IMHO.

But we can go further with this. We can let our interface extend from the FormGroup and the use only the interface inside our component.

interface UserForm
  extends FormGroup<{
    firstName: FormControl<string>;
    lastName: FormControl<string>;
    age: FormControl<number>;
  }> {}

@Component(/* ... */)
export class FormSimpleGroupComponent implements OnInit {
  myForm: UserForm;

  constructor(private formBuilder: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.formBuilder.group({
      firstName: '',
      lastName: '',
      age: 0,
    });
  }

  onSubmit() {
    const formValue = this.myForm.value;
    console.log(formValue);
  }
}

With this, our types are completely separated from our logic, but we have all the advantages of the new Angular typed reactive forms.