Typed Form in Angular 14 shows `<any>` instead of types
Asked Answered
L

3

6

While being on the latest version of Angular (version 14), it seems that I am not doing something well and thus strictly typed reactive forms are not working as expected.

Form is initialized inside ngOnInit using injected FormBuilder.

 public form!: FormGroup;

 constructor(private formBuilder: FormBuilder) {}

 ngOnInit(): void {
    this.initializeForm();
  }

  private initializeForm(): void {
    this.form = this.formBuilder.group({
      title: ['', [Validators.required, Validators.minLength(3)]],
      content: ['', Validators.required],
    });
  }

Now when I try to access controls of the form there is no autocomplete and type is FormGroup<any>. Also it doesn't throw the error when trying to access controls which are not present on the FormGroup object.

  • Example:

enter image description here

  • package.json

enter image description here

  • Angular official documentation:

https://angular.io/guide/typed-forms

Lissotrichous answered 19/6, 2022 at 17:38 Comment(0)
C
6

TL;DR: The way TypeScript identifies its "types" mostly due to interfaces and context. My recommendation to solve the use is to explicitly create an interface and pass it to the form as it cannot be inferred.


In most cases; Angular will infer the form type as long as its initialized with the proper types in declaration. This answer acknowledged that specific use case.

Note about inferred types, inferred types require more work for refactorization. Also, interfaces you can be used to easily identify the object and attribute types by just looking its declaration, the later cannot be done using inferred types. Is my recommendation to always prefer interfaces over inferred types, of course this note comes from my own experience.


Your use case:

In the example you provided, there's no way way to infer the form type, you've to explicitly tell. I've made this working example of a typed form. The official documentation you included in your question kind of already gives you most of the answer.

To remove the <any> you see in your autocomplete, just implement your own interface:

export interface IMainForm {
    title: FormControl<string>;
    content: FormControl<string>;
}
public form!: FormGroup<IMainForm>; // <--- Use your interface

Also, your implementation of this.formBuilder.group is deprecated because is not typesafe. You'll need to use the overload with AbstractControlOptions instead (not the array one).

private initializeForm(): void {
  this.form = this.formBuilder.group({
    title: new FormControl<string|null>(null, [Validators.required, Validators.minLength(3)]),
    content: new FormControl<string|null>(null, Validators.required)
  });
}

As you can see in the following image, with those changes you'll see the typed value {Partial<{ title: string; content: string; }>}. enter image description here


You can check how type inference works for more detail in typescriptlang, here´s a small extract:

Simple type let x = 4 being number

The type of the x variable is inferred to be number. The kind of inference takes place when initializing variables and members, setting parameter default values, and determining function return types.

Best common type let x = [0, 1, null] being of type (number | null)[]

To infer the type of x in the example above, we must consider the type of each array element. Here we are given two choices for the type of the array: number and null. The best common type algorithm considers each candidate type, and picks the type that is compatible with all the other candidates.

Contextual types window.onmousedown = function (mouseEvent) { ... }, being MouseEvent

Contextual typing applies in many cases. Common cases include arguments to function calls, right hand sides of assignments, type assertions, members of object and array literals, and return statements. The contextual type also acts as a candidate type in best common type.

Cassondracassoulet answered 22/6, 2022 at 0:18 Comment(1)
Actually I used this.formBuilder.nonNullable.group and it seems to work fineLissotrichous
H
4

Here is a solution that doesn't require you to define interface but use the inferred type

form: ReturnType<typeof this.initializeForm>
...
private initializeForm() {
  return this.formBuilder.group({
    title: ['', [Validators.required, Validators.minLength(3)]],
    content: ['', Validators.required],
  });
}
Hemistich answered 2/8, 2022 at 11:25 Comment(1)
Quite an elegant way if you need to initialize your form later than construction time(e.g. to avoid UI flicker if you want to set some input values after a response arrives)Slr
H
0

The Angular documentation is not nearly clear enough, but the reason type checking is not working for your strictly typed Reactive Form is because you are initialising the form in the OnInit hook.

Instead, you need to initialise your form directly, this allows the types to be inferred. If you don't do this and just give it a FormGroup type it defaults implicitly to FormGroup<any>, which is why you're not seeing the benefits of Angular's typed forms.

Your example should work when refactored like so:

@Component({
  selector: 'example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.css']
})
export class ExampleComponent {

   public form = this.formBuilder.group({
      title: ['', [Validators.required, Validators.minLength(3)]],
      content: ['', Validators.required],
  });

  constructor(private formBuilder: FormBuilder) {}
}

I've added a working example of this, and included a line that is commented out which attempts to set the wrong type, if you uncomment it you will see a type error which the editor will warn you about.

Note - I don't want to be rude, but @luiscla27's accepted answer to this question is wrong and unhelpful to anyone who finds this SO question who has run into the common pitfall of not directly initialising their typed reactive form, as the OP did.

Honorine answered 21/11, 2022 at 18:18 Comment(7)
Initializing the form somewhere else but it's declaration its a use case, not a pitfall :)Cassondracassoulet
Initialising the form indirectly is a valid scenario, and you have provided a valid workaround for that scenario, but the OP was asking why their form wasn't working after thinking they had followed Angular's documentation (as will others finding this question) because the documentation doesn't make clear enough that the intialisation has to be direct to get the benefits without using an interface. The correct answer is that the OP needs to initialise the form directly, and if they do not want to do that for some reason, then they can use an interface as you've shown.Honorine
Is my preference to use interfaces than inferred types because you can reuse them later in DTO's (or any other variable) without running in the issue of having wrong types assigned after code modifications or when new fields are added. Interfaces are good :) Even though your answer may require less effort to implement, I'll still suggest using interfaces for the sake of code maintenance.Cassondracassoulet
As I've said, your preference is a valid approach, there's nothing wrong with using interfaces like that. My point is that it's not the right answer to the question of why the OP, having tried to follow Angular's own documentation, had a form that wasn't enforcing types as expected.Honorine
I just wanted to point out in my last comment that even though you're right about inferred types. It's still better to use interfaces as in the long run they bring more benefits. Inferred types have been creating bugs in big projects to me for a long time now as they are hard to refactor. Please use interfaces, don't infer types.Cassondracassoulet
I just want to add my 2 cts and say that, as someone having the same question as the OP, the scenario described by @JoshDoug in his previous comment is exactly what has happened to me. I found a workaround in the accepted answer, but not an actual answer to my use case. It took a second look. I don't plan to use my form models outside of my View Layer (a bad practice IMO anyway because it couples View and Model @luiscla27), inferred types are perfect for me.Slr
@Slr thanks, this is exactly what I was trying to explain to luiscla27. Nothing wrong with their approach, but it doesn't explain why the types weren't working for the OP so it doesn't answer the question!Honorine

© 2022 - 2025 — McMap. All rights reserved.