Typed Forms in Angular
Tags: javascript, angular, typescript, webdev
Published: 2022-10-20 18:47:47

Angular version 14 is a feature-packed release that brings new APIs, functionality, and a developer experience.
This includes one of the most requested feature improvements in Angular: typed forms.

In this article, you'll learn:

- How to migrate to the new strongly-typed forms API in Angular version 14
- Typed a `FormControl`
- Typed a `FormGroup`
- Typed a `FormArray`
- New `FormRecord`
- Mixing typed and untyped controls
- Non-nullable controls

Let's dive in!

## Typed Forms API FTW 🎉

If you have been using Angular's forms API, whether template-based or reactive forms, you likely have noticed that prior to Angular 14 much of the API surface had a *lot* of `any` types.

Angular (version 2, not AngularJS) was released in September of 2016 (after Flash died but before build tools took the world by storm). Not long after the release, developers started to note that the API was both not type-safe (or null-safe).
In fact, [the most upvoted and commented on issue](https://github.com/angular/angular/issues/13721) in the GitHub repository was created December of 2016 to report this problem.

Angular 14 introduces reactive form APIs that are both type-safe and null-safe. This is a big win for using reactive forms in Angular.
Unfortunately, the template-driven forms API is currently not strongly typed.

## Migrating to Angular 14

To get started, you need to update your project to use Angular 14 to take advantage of the new APIs.

```bash
ng update @angular/cli @angular/core
```

Once your project is updated, you'll not that instance of `FormControl` and `FormGroup` have been migrated to the untyped classes:

- `FormControl` is migrated to `UntypedFormControl`
- `FormGroup` is migrated to `UntypedFormGroup`

## Typed `FormControl`

In Angular 14, the `FormControl` class now has a TypeScript generic type of `TValue` whose default type assignment is `any`. What does this mean?

Prior to Angular 14, when accessing properties such as `value` the type was `any`, and when using the method `setValue`, it would accept `any` argument value.

Here are just a few of the class members that have been updated to use the `TValue` generic type:

- `setValue(value: TValue)`
- `patchValue(value: TValue)`
- `reset(formState: TValue)`
- `getRawValue(): TValue`
- `value: TValue`

This is a big win for preventing runtime exceptions and regressions due to type errors!

It's important to note that the `TValue` generic type is inferred by TypeScript when newing-up a new `FormControl` instance, so it's not necessary to specify the generic type.
Let's look at an example.

```tsx
const control = new FormControl('Mike');
```

Note, we did not specify the `TValue` generic type above.
Rather, the type is inferred by the `value` argument specified to constructor function when creating a new instance of the `FormControl` class.

## Typed `FormGroup`

The purpose of Angular's `FormGroup` is to group form controls logically in order to track their state (value, validity, etc.) together.
Angular 14 also introduces updated typings for the `FormGroup` API.

Let's look at an example.

```tsx
interface SignUpForm {
  name: string;
  email: string;
  subscribe?: string;
}

const signUpFormGroup = new FormGroup<SignUpForm>({
  name: new FormControl(''),
  email: new FormControl('')
});
```

Let's quickly review the code above:

- First, we define a new `SignUpForm` interface.
- The interface is *only necessary if we have optional form controls*. In this example, the `subscribe` control can be dynamically added and removed from the group.
- Next, we create a new `FormGroup` instance, and since we have an optional control, we specify the generic type to the `SignUpForm` interface.
- The `name` control's value is typed: `string | null`
- The `email` control's value is typed: `string | null`
- The `subscribe` control value is typed: `string | null | undefined`
- The `signUpFormGroup` value is typed: `Partial<{ name: string | null; email: string | null; subscribe: string | null | undefined }>`

You may have noticed that the control values can be `null` or, if optional, `undefined`.
This is because Angular's form APIs allow for null values.
For example, if we invoke the `reset()` method on a control, the value is not set to the original/seed value, rather, it is set to `null`.
We'll show how to override this behavior below.

It should also be noted that the `TValue` for each control is also inferred as we previously learned.

## Typed `FormArray`

Currently, TypeScript arrays are homogeneous.
This means that TypeScript expects that every item in an array is of the same type (including `any`).
For the sake of clarity, this is not to be confused with tuples which can have unambiguous types.

With that said, typed `FormArray` instances require that all controls are homogeneously typed.
If a `FormArray` requires heterogeneous types then the use of the `UntypedFormArray` is recommended.

In this example we'll create a new `FormArray` with an initial control.

```tsx
lineItems = new FormArray([
  new FormControl('')
]);
```

Because we have defined the `FormArray` to include a control whose `TValue` is a `string`, the type of the array is inferred as: `FormArray<AbstractControl<string, string>>`.
The compiler now expects that each item in the array is a control whose `TValue` is a `string`.

If we attempt to add a new control to the array whose `TValue` is *not* a string, the compiler will throw an exception indicating:

```tsx
lineItems.push(new FormControl(0));
```

The compiler exception indicates that there is an error.

```
Argument of type 'FormControl<number>' is not assignable to parameter of type 'AbstractControl<string, string>'.
Types of property 'value' are incompatible.
Type 'number' is not assignable to type 'string'.
```

This is a good thing! 😌

Ok, but what if our array of controls is initially empty?
In that case, we can specify the generic type of the controls:

```tsx
lineItems = new FormArray<AbstractControl<string>>([]);
```

In the example above, we use the `AbstractControl` type and specify the `TValue` to be of type `string` as we expect every control's value to be a `string`.

## New `FormRecord`

As we learned previously the `FormGroup` in Angular 14+ supports specifying a group of controls whose `TValue` types are known when creating the group, even when creating optional controls within the group.
However, what about a group of controls whose `TValue` is not known when creating the group.
To meet this use case, Angular 14 ships with a new `FormRecord` class.

Let's look at an example.

```tsx
formGroup = new FormRecord<AbstractControl<string>>({});
```

Now that we have created the group of controls using the `FormRecord` class, we can start to add (and/or remove) controls from the group.

```tsx
formGroup.addControl('street', new FormControl(''));
```

This code works as expected.
A new control is added to the group that meets the type definition.
However, what happens if we attempt to add a new control whose `TValue` is a `number`?

```tsx
this.formGroup.addControl('no', new FormControl(0));
```

Again, the compiler throws an exception indicating that we cannot add this control to the group due to the type constraints.

```
Argument of type 'FormControl<number>' is not assignable to parameter of type 'AbstractControl<string, string>'.
Types of property 'value' are incompatible.
Type 'number' is not assignable to type 'string'.
```

The `UntypedFormGroup` class supports the user requirements in the event that we need a group of controls whose values are heterogeneous.

## Mixed Types

We can declare mixed types of controls when working with a group of controls of heterogeneous types as long as we declare the controls when creating the group.
Further, we can use the `UntypedFormControl` class to declare a control whose `TValue` type is `any` (under the hood, this is a type whose generic type is preset to `any`).

Let's look at an example.

```tsx
export class AppComponent implements OnInit  {
  formGroup = new FormGroup({
    street: new FormControl(''),
    no: new FormControl(0),
    postalCode: new UntypedFormControl()
  });

  ngOnInit(): void {
    const street = this.formGroup.value.street;
    const no = this.formGroup.get('no').value;
    this.formGroup.get('postalCode').setValue(12345);
    this.formGroup.get('postalCode').setValue('ABC123');
  }
}
```

This all works as we expect.
We have a few controls, each with their own `TValue`, including the untyped control whose `TValue` is `any`.

## Non-nullable Controls

If you recall from earlier, we mentioned that resetting a form control state will set the value to `null`, not the initial/seed value as we might expect.
With non-nullable form controls, we can explicitly instruct the compiler that the value is reset to the initial value when invoking the `reset()` method.

Let's look at an example

```tsx
export class AppComponent implements OnInit {
  formGroup = new FormGroup({
    street: new FormControl('', { nonNullable: true })
  });

  ngOnInit(): void {
    this.formGroup.get('street').reset();
    const street = this.formGroup.value.street;
    console.log(street); // empty string, NOT null
  }
}
```

## Key Takeaways

There are a few key takeaways when using Angular 14's updated forms API:

- The `FormControl`, `FormGroup`, and `FormArray` signatures have been updated to support TypeScript generics.
- The `TValue` type is inferred where possible.
- `FormGroup` supports enumerated, homogeneous, and optional controls.
- `FormRecord` supports non-enumerated (dynamic) and homogenous controls.
- `UntypedFormControl` is a non strongly-typed `FormControl`. Or, in other words, the `TValue` type is preset to `any`.
- `UntypedFormGroup` is a non strongly-typed `FormGroup`.
- `FormArray` supports homogeneously typed controls.
- `UntypedFormArray` is a non strongly-typed `FormArray`.
- The `nonNullable` supports resetting a control state to the initial/seed value.
- Through strict types and template type checking, we can avoid runtime exceptions and reduce regressions.

## Need Help?

Do you have questions? We can help! [LiveLoveApp provides angular consulting](https://liveloveapp.com/services/angular-development). We can answer your questions, perform an architecture review of your application and help you to implement best practices to improve your software development performance.