Skip to content

Wrong FormControl type inference when created with FormBuild through group() with Validators and an array as value  #46442

Closed

Description

@Mikastark

Which @angular/* package(s) are the source of the bug?

forms

Is this a regression?

No

Description

When creating a FormGroup with the new ng14 FormBuilder, if you try to add a control with both array value and validator(s) the infered type for this control become wrong. It seems the validator(s) are considered as value so we get a bad union typing.

/**
 * Wrong type!
 * FormGroup<{
 *   test: FormControl<number[] | ((control: AbstractControl<any, any>) => ValidationErrors | null)>;
 *  }>
 */
const form = fb.nonNullable.group({
  test: [[1,2,3], Validators.required]
})

If you use the long way to add control, everything works just fine but we loose FormBuilder.group() shorthand synthax benefit

/**
 * Good type!
 * FormGroup<{
 *   test: FormControl<number[]>;
 *  }>
 */
const form = fb.nonNullable.group({
  test: fb.nonNullable.control([1,2,3], Validators.required)
})

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/angular-ivy-cw97ml?file=src/app/app.component.ts

Please provide the exception or error you saw

Type 'FormGroup<{ test: FormControl<number[] | ((control: AbstractControl<any, any>) => ValidationErrors)>; }>' is not assignable to type 'FormGroup<{ test: FormControl<number[]>; }>'.
  Types of property 'controls' are incompatible.
    Type '{ test: FormControl<number[] | ((control: AbstractControl<any, any>) => ValidationErrors)>; }' is not assignable to type '{ test: FormControl<number[]>; }'.(2322)

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 14.0.2
Node: 16.15.1
Package Manager: npm 8.12.2 
OS: linux x64

Angular: 14.0.2
... animations, cdk, cli, common, compiler, compiler-cli, core
... forms, material, platform-browser, platform-browser-dynamic
... router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1400.2
@angular-devkit/build-angular   14.0.2
@angular-devkit/core            14.0.2
@angular-devkit/schematics      14.0.2
@angular/flex-layout            13.0.0-beta.38
@schematics/angular             14.0.2
rxjs                            7.5.5
typescript                      4.7.4

Anything else?

No response

Activity

  1. added this to the needsTriage milestone on Jun 21, 2022
  2. dylhunn commented on Jun 22, 2022

    @dylhunn
    Contributor

    This is happening because the nested arrays confuse the type inference. The root cause is that the FormBuilder API is so polymorphic that it's very tricky to get the types to always infer correctly. Fixing this in the short term will require a bit more investigation, but in the long run we should upgrade FormBuilder to a better API.

    As a workaround, try constructing your FormArray directly as the value for test (e.g. with new FormArray()).

  3. added
    P2The issue is important to a large percentage of users, with a workaround
    on Jun 22, 2022
  4. modified the milestones: needsTriage, Backlog on Jun 22, 2022
  5. Mikastark commented on Jun 23, 2022

    @Mikastark
    Author

    @dylhunn thank you for the comment. For now on I will stop using short array synthax as it's confusing anyway.

    And to be clear, in the example it's a FormControl that is used with array value, not a FormArray. So I guess you meant using new FormControl<string[]>([]) or fb.control<string[]>([]) do you ?

  6. dylhunn commented on Jun 23, 2022

    @dylhunn
    Contributor

    Yes, if your FormControl takes an array value, use new FormControl<string[]>([]).

  7. sysmat commented on Jun 29, 2022

    @sysmat
    • expects validators and asyncValidators for AbstractControlOptions when using AbstractControl should support the type of strong typed FormGroup.
    • why should this be mandatory to access formGroup controls by props not by get('name of control')
    formOptions: AbstractControlOptions = {
        validators: this.passEqualValidator
      };
    
      passForm: FormGroup<IPasswordForm> = new FormBuilder().group(
        {
          pass1: new FormControl(null, Validators.compose([Validators.required, Validators.minLength(8), Validators.maxLength(25), isAscii])),
          pass2: new FormControl(null, Validators.compose([Validators.required, Validators.minLength(8), Validators.maxLength(25), isAscii]))
        },
        this.formOptions
      );
    
    // getting control by string is very bad when doing some refactoring
    passEqualValidator(form: AbstractControl<IPasswordForm>): ValidationErrors {
        const err = { notEq: true };
        let condition = false;
        if (form.get('pass1').value && form.get('pass2').value) {
          condition = form.get('pass1').value !== form.get('pass2').value;
        }
    
        return condition ? err : null;
      }
  8. Mikastark commented on Jun 29, 2022

    @Mikastark
    Author

    @sysmat Sorry to say that, but I think your comment is off topic and should be posted on Stackoverflow. Anyway I will try to help you.

    You probably missunderstood the AbstractControl<T> generic type. Unlike FormGroup<T>, it represents the value schema (in your case { pass1: string | null, pass2: string | null }). Also instead of using .get() you should use .value as it's more relevant and simplier in your case. Also, you should use DI instead of instanciate FormBuilder yourself or use new FormGroup(...). In few words, here is your revised and errorless code

    // I think it deserves to be outside you component and maybe in it own file
    export const passEqualValidator: ValidatorFn = (control: AbstractControl<{ pass1: string | null, pass2: string | null }>) => {
      return control.value.pass1 === control.value.pass2 ? null : { notEq: true };
    }
    
    // The code after this point should be inside your component ...
    
    // Declare the shape of your form
    passForm: FormGroup<{
      pass1: FormControl<string | null>, // you are not setting controls as nonNullable so the value can be null (cf. doc)
      pass2: FormControl<string | null>,
    }>
    
    // use DI to inject FormBuilder
    constructor(fb: FormBuilder) {
      // you don't need `Validators.compose()`, an array of validators is enough
      const passControlValidators = [Validators.required, Validators.minLength(8), Validators.maxLength(25), isAscii]
      this.passForm = fb.group(
        {
          pass1: fb.control('', passControlValidators),
          pass2: fb.control('', passControlValidators)
        },
        {
          validators: passEqualValidator
        }
      );
    }
  9. destus90 commented on Jul 1, 2022

    @destus90
    Contributor

    I think this happens not only when you are using an array value for your FormControl.

    For this case we are getting a bad typing too:

    const form = this.fb.group({
      analyzedFeatures: [null, Validators.required],
    });
    

    image

  10. Mikastark commented on Jul 1, 2022

    @Mikastark
    Author

    After trying many ways of initializing forms since version 14, I think the form builder doesn't bring much value than instanciate directly form controls. Eventually the only benefit is the .nonNullable shortcut. This benefit could be replaced with an eventual new NonNullableFormControl or NonNullableFormArray class (in the case of FormArrays I think it would be better to declare them non nullable and initialize them with an empty array by default)
    But what is the point of this comment ? IMO dropping FormBuilder feature whould give Angular team more time and resource to focus on other more complex and needed features

    Of course it's just my opinion

  11. dylhunn commented on Aug 4, 2022

    @dylhunn
    Contributor

    Edit -- removing this response. Inner ControlConfig arrays are disallowed, so maybe this can be fixed without a whole new abstraction.

  12. dylhunn commented on Aug 4, 2022

    @dylhunn
    Contributor

    @destus90 I actually cannot reproduce your specific example. Mind linking me to a StackBlitz?

  13. iErKy commented on Aug 11, 2022

    @iErKy

    Hello regarding this topic i have a concern.
    I'm trying to use NonNullableFormBuilder to create my FormGroup with nonNullable FormControl's but for some reason if i need to still put nonNullable: true into the FormControl. I link an stackblitz example, apreciate if someone could tell me if this is related to this bug or it's another bug which should be reported aswell.

    https://stackblitz.com/edit/angular-ivy-uzcbsd?file=src/app/app.component.ts

    Thanks in advance.

  14. dylhunn commented on Aug 18, 2022

    @dylhunn
    Contributor

    @destus90 and @Mikastark: This should now be fixed and released in 14.2.0-rc.0. Does it solve the issue for both of you? Thanks!

    @iErKy Can you please open a new issue? That will help us keep our topics separate. Thanks!

  15. destus90 commented on Aug 30, 2022

    @destus90
    Contributor

    @dylhunn

    works fine, thanks!

  16. Mikastark commented on Aug 30, 2022

    @Mikastark
    Author

    I don’t use RC version of angular. Waiting for final 14.2 version.

  17. destus90 commented on Aug 31, 2022

    @destus90
    Contributor

    @Mikastark

    14.2 was published some days ago...

  18. angular-automatic-lock-bot commented on Oct 1, 2022

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  19. locked and limited conversation to collaborators on Oct 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions