Angular Typesafe Reactive Forms With TypeScript

(Update - March 2020 I finally created a npm package angular-typesafe-reactive-forms-helper)

Problem

Working with more complex Reactive Forms, it was challenging to remember how the form was bound to my FormGroup object.

I wanted intellisense to help me.

This is because the FormGroup class has a ‘value’ property which returns ‘any’, the cause of my frustrations.

Good luck renaming property names!

1
2
3
4
5
//my dream and challenge
get value: CustomType; // <-- somehow the angular code must return a CustomType

//from Angular FormGroup class
get value: any;

Another frustration I faced was referring to the form properties names as ‘string’ values. It quickly got out of hand when I renamed property names.

1
2
3
4
5
6
7
8
//example of usage
this.form.get('heroName').patchValue('He-Man');

//my dream and challenge
this.form.get(x => x.heroName).patchValue('He-Man');

//from Angular FormGroup class
get(path: Array<string|number>|string): AbstractControl|null

Angular team is considering implementing Typesafe Reactive Forms

Here are the issues worth tracking:
* Proposal - ReactiveForms: add AbstractControl.getChild<T> method
* Reactive forms are not strongly typed

Can’t wait for that to be fully implemented…until then, I’ll hack my way to typesafety :)

Solution

Quick Demo

The Interface used for all the code samples

1
2
3
4
5
6
interface IHeroFormModel {
name: string,
secretLairs: Array<Address>,
power: string,
sidekick: string
}

Intellisense on FormGroupTypeSafe<T>.value:

FormGroupTypeSafe.value Intellisense

Intellisense on FormGroupTypeSafe<T>.getSafe and then patching the value:

FormGroupTypeSafe.getSafe Intellisense

By having a lambda like expression instead of a ‘string’ property name, makes refactoring so much easier.

Typesafe Angular Reactive Forms Sample

I modified the Angular Reactive Forms Sample with my Typesafe Angular Reactive Forms Sample.

Search for TypeSafe Reactive Forms Changes to find all the changes. I commented out the original angular code and replaced it with the Typesafe way as a quick reference.

Files to focus on:

  • app/app.modules.ts
  • app/hero-detial.component.ts
  • app/angular-reactive-forms-helper.ts

* app/app.modules.ts *

Two things of importance here:

  • import the FormBuilderTypeSafe class
  • add to provider list for the Dependency Injection

See Typesafe Angular Reactive Forms Sample for details.

* app/hero-detial.component.ts *

  1. Define an interface of your form model.

    1
    2
    3
    4
    5
    6
    7
    //interface used with FormGroupTypeSafe<T>
    interface IHeroFormModel {
    name: string,
    secretLairs: Array<Address>,
    power: string,
    sidekick: string
    }
  2. Declare your new FormGroupTypeSafe form with the help of TypeScript’s generics.

    1
    2
    3
    4
    /* TypeSafe Reactive Forms Changes */
    //old code
    //heroForm: FormGroup;
    heroForm: FormGroupTypeSafe<IHeroFormModel>;
  3. Inject FormBuilderTypeSafe.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    constructor(
    /* TypeSafe Reactive Forms Changes */
    //old code - private fb: FormBuilder,
    private fb: FormBuilderTypeSafe,
    private heroService: HeroService) {

    this.createForm();
    this.logNameChange();
    }
  4. Create your form group with Interfaces (contracts).

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // old code
    // this.heroForm = this.fb.group({
    // name: '',
    // secretLairs: this.fb.array([]),
    // power: '',
    // sidekick: ''
    // });


    this.heroForm = this.fb.group<IHeroFormModel>({
    name: new FormControl(''),
    secretLairs: new FormControl([]),
    power: new FormControl(''),
    sidekick: new FormControl('')
    });

    //***** Nested type sample *****
    interface IAddressModel {
    suburb: string;
    postcode: string;
    }

    interface ICustomerModel {
    name: string;
    address: IAddressModel
    }

    this.form = this.fb.group<ICustomerModel>({
    name: new FormControl(null, [Validators.required]),
    address: this.formBuilder.group<IAddressModel>({
    suburb: new FormControl(''),
    postcode: new FormControl('', [Validators.required]),
    })
    });

    Benefits TypeScript provide:

    • Detecting errors (contract mismatch): Let’s say we change the ‘name’ property, ‘name’ => ‘nameNew’, but the IHeroFormModel.name did not change, TypeScript will raise the alarm.

    • Renaming properties: Super easy, I’m using VSCode and it’s as easy as F2, rename the property, enter…and TypeScript will do the rest. No more search and replace.

    • No more misspelled properties: Intellisense ensures you get the correct property name everytime. With string property names a mistake can easily creep in.

    • Guidance on how to create the form group: Code sample below will have TypeScript raise the alarm when attempting to set ‘name’ to ‘’, FormGroupControlsOf<T> returns FormControl | FormGroup.

      message: ‘Argument of type ‘{ name: string; secretLairs: FormControl; power: FormControl; sidekick: FormControl; }’ is not assignable to parameter of type ‘FormGroupControlsOf‘.
      Types of property ‘name’ are incompatible.
      Type ‘string’ is not assignable to type ‘FormControl | FormGroup’.’
      at: ‘1, 49’
      source: ‘ts’

1
2
3
4
5
6
this.heroForm = this.fb.group<IHeroFormModel>({
name: '',
secretLairs: new FormControl([]),
power: new FormControl(''),
sidekick: new FormControl('')
});

5. Use the typesafe form.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//old code 
//this.heroForm.get('name').patchValue('new hero');
this.heroForm.getSafe(x => x.name).patchValue('new Hero');

//old code
//this.heroForm.setControl('secretLairs', addressFormArray);
this.heroForm.setControlSafe(x => x.secretLairs, addressFormArray);

formatAddressToOneLine(addressIndex: number) : string {
//When using angular's FormGroup no intellisense is provided
//when typing 'this.heroForm.value.'.
//The FormGroupTypeSafe form will give you intellisense.
//The benefit is when you rename property names,
//TypeScript will make sure you change all the references

//old code => this.heroForm.value.secretLairs[addressIndex] returns 'any',
//then use 'as Address' to get it to be an Address object for intellisense to work
//let address = this.heroForm.value.secretLairs[addressIndex] as Address;
//No more 'as', TypeScript knows it is an Address
let address = this.heroForm.value.secretLairs[addressIndex];
return address.street + ", " + address.city + ", " + address.state;
}

* app/angular-reactive-forms-helper.ts *

In order to make this work as closely as possible to the angular way I created an abstract class FormGroupTypeSafe<T> which derives from angular’s FormGroup.
It also ensures that exisitng code will not break.
I also created two functions: getSafe, setControlSafe.
Those were the only functions I needed for the Typesafe Angular Reactive Forms Sample.
If you need more, add them :)

You can find the implementation of angular-reactive-forms-helper.ts on my Github account or have a look at the Plunker example: Typesafe Angular Reactive Forms Sample.

Conclusion

I don’t misspell property names anymore and refactoring Reactive Forms is back to being a trivial task.
I think the extra bit of ceremony is absolutely worth it.

Use it…don’t use it :)