DEV Community

Trong Nguyen
Trong Nguyen

Posted on

Angular Reactive Forms - Some Type Safety~

Reactive forms in angular are a very powerful way to create forms. Unfortunately, the form builder does not support any type safety.
Small typos can go unnoticed and using models for multiple forms can be frustrating to maintain.

But thanks to TypeScript there are some ways to overcome those issues :).

The problem

interface Person {
  firstname: string;
  lastname: string;
}

export class AppComponent {
  form: FormGroup;

  constructor(fb: FormBuilder) {
    this.form = fb.group({
      firstName: [null],
      lastNamee: [null]
    });
  }
}

By creating a form the default way, some general issues are not covered. The form does not match the defined model. At the latest when using your form value with an API or using the formControlName directive you might notice the error.

Using Record

Record<K, P> is a utility type provided by TypeScript. It requires two type arguments (documentation).

interface Person {
  firstname: string;
  lastname: string;
}

export class AppComponent {
  form: FormGroup;

  constructor(fb: FormBuilder) {
    const form: Record<keyof Person, any> = {
      firstname: [null],
      lastname: [null]
    };
    this.form = fb.group(form);
  }
}

Using a custom type

If you'd like to avoid repeating the Record<keyof T, any> syntax, you can declare a type for general uses. This custom type can also be declared more specific. But for now, we go with the any type to accomplish the same result as before.

type FormGroupModel<T> = {
  [x in keyof T]: any;
}

interface Person {
  firstname: string;
  lastname: string;
}

export class AppComponent {
  form: FormGroup;

  constructor(fb: FormBuilder) {
    const form: FormGroupModel<Person> = {
      firstname: [null],
      lastname: [null]
    };
    this.form = fb.group(form);
  }
}

Dive deeper

To make it more type safe, like passing the correct value or validators, we can enhance the FormGroupModel a little bit more.

type ValidatorModel<T, K> = T | K | (T | K)[];

type FormGroupModel<T> = {
  [x in keyof T]: [
    T[x],
    ValidatorModel<Validator, ValidatorFn>?,
    ValidatorModel<AsyncValidator, AsyncValidatorFn>?
  ];
};

Changing the any type to this ominous code makes it more restricted. The type now accepts an array with up to 3 arguments.

The first argument in the array (T[x]) states, that the initial value has to match with the defined model.

interface Person {
  firstname: string;
  lastname: string;
}

const form: FormGroupModel<Person> = {
  firstname: ['Randy'], // valid
  lastname: [123]       // invalid
};

The second argument can be a Validator or a ValidatorFn or a set of Validator and ValidatorFn.

const form: FormGroupModel<Person> = {
  firstname: ['Randy', Validators.required],
  lastname: ['McRandom', [Validators.required, Validators.minLength(5)]]
};

The third argument works like the second, but for AsyncValidator and AsyncValidatorFn.

Summary

With those approaches, the form is enforced to be built correctly. The biggest benefit is, that when the model changes, the compiler will complain that the form does not match. This will make your project way more maintainable 👍.

Top comments (1)

Collapse
 
louis profile image
Louis Augry

I approve 100%