Search code examples
javascriptangulartypescriptangular-reactive-forms

Declaring the correct type for an Angular Reactive Forms cross property validator?


In this tutorial the author implements a password check validator like this:

  MatchPassword(password: string, confirmPassword: string) {
    return (formGroup: FormGroup) => {
      const passwordControl = formGroup.controls[password];
      const confirmPasswordControl = formGroup.controls[confirmPassword];

      if (!passwordControl || !confirmPasswordControl) {
        return null;
      }

      if (confirmPasswordControl.errors && !confirmPasswordControl.errors.passwordMismatch) {
        return null;
      }

      if (passwordControl.value !== confirmPasswordControl.value) {
        confirmPasswordControl.setErrors({ passwordMismatch: true });
      } else {
        confirmPasswordControl.setErrors(null);
      }
    }
  }

No return type is set for the function. This leads to linting errors in Angular 13.

I've tried to fix the linting errors by changing the signature of the cross property validation function to this:

   MatchPassword(password: string, confirmPassword: string):ValidatorFn | ValidatorFn[] | AbstractControlOptions | null | undefined { ...

However now the linter errors on the signature of the validation function that is returned:

...
     return (formGroup: FormGroup):ValidatorFn | ValidatorFn[] | AbstractControlOptions | null | undefined => {
...
Type '(formGroup: FormGroup) => ValidatorFn | ValidatorFn[] | AbstractControlOptions | null | undefined' is not assignable to type 'ValidatorFn | AbstractControlOptions | ValidatorFn[] | null | undefined'.
  Type '(formGroup: FormGroup) => ValidatorFn | ValidatorFn[] | AbstractControlOptions | null | undefined' is not assignable to type 'ValidatorFn'.
    Types of parameters 'formGroup' and 'control' are incompatible.
      Type 'AbstractControl' is missing the following properties from type 'FormGroup': controls, registerControl, addControl, removeControl, and 3 more.ts(2322)

This does not happen on Stackblitz, only on new Angular 13 projects, thus I can't create a Stackblitz for it.

Anyone know how to declare the signature for Reactive Form Validators that perform cross property validation?

LINTING PROGRESSION

For those wondering, the very first linting error occurs within the registration component that is assigning the cross property validator.

It creates the FormGroup instance and the custom cross property validator is assigned as the third argument like this:

new FormGroup('', ..., this.v. MatchPassword('password', 'confirmPassword'))

And that is where the first linting error occurs.

** Side Note ** this.v is the injected validation service containing the MatchPassword function.

The linting error generated says this:

Argument of type '(formGroup: FormGroup) => null | undefined' is not assignable to parameter of type 'ValidatorFn | ValidatorFn[] | AbstractControlOptions | null | undefined'.
  Type '(formGroup: FormGroup) => null | undefined' is not assignable to type 'ValidatorFn'.
    Types of parameters 'formGroup' and 'control' are incompatible.
      Type 'AbstractControl' is missing the following properties from type 'FormGroup': controls, registerControl, addControl, removeControl, and 3 more.ts(2345)

Solution

  • if you'd like to do cross field validation that is easy because when passed into the new FormGroup(...) the validator gets the actual FormGroup. but for typing Angular has (control: AbstractControl) and that is fine because FormGroup is actually a type of AbstractControl. so all you need to do is to check that this is a FormGroup and not one of the other types of AbstractControl s so you could do

       MatchPassword(password: string, confirmPassword: string) {
         return (control: AbstractControl) => {
             const isFormGroup = control instanceof FormGroup;
             if (!isFormGroup) {
                return null
             }
            // do what ever you'd like...
            const passwordControl = formGroup.controls[password];
            const confirmPasswordControl = formGroup.controls[confirmPassword];