W3cubDocs

/Angular

Typed Forms

As of Angular 14, reactive forms are strictly typed by default.

Prerequisites

As background for this guide, you should already be familiar with Angular Reactive Forms.

Overview of Typed Forms

With Angular reactive forms, you explicitly specify a form model. As a simple example, consider this basic user login form:

const login = new FormGroup({
    email: new FormControl(''),
    password: new FormControl(''),
});

Angular provides many APIs for interacting with this FormGroup. For example, you may call login.value, login.controls, login.patchValue, etc. (For a full API reference, see the API documentation.)

In previous Angular versions, most of these APIs included any somewhere in their types, and interacting with the structure of the controls, or the values themselves, was not type-safe. For example: you could write the following invalid code:

const emailDomain = login.value.email.domain;

With strictly typed reactive forms, the above code does not compile, because there is no domain property on email.

In addition to the added safety, the types enable a variety of other improvements, such as better autocomplete in IDEs, and an explicit way to specify form structure.

These improvements currently apply only to reactive forms (not template-driven forms).

Automated Untyped Forms Migration

When upgrading to Angular 14, an included migration will automatically replace all the forms classes in your code with corresponding untyped versions. For example, the snippet from above would become:

const login = new UntypedFormGroup({
    email: new UntypedFormControl(''),
    password: new UntypedFormControl(''),
});

Each Untyped symbol has exactly the same semantics as in previous Angular versions, so your application should continue to compile as before. By removing the Untyped prefixes, you can incrementally enable the types.

FormControl: Getting Started

The simplest possible form consists of a single control:

const email = new FormControl('[email protected]');

This control will be automatically inferred to have the type FormControl<string|null>. TypeScript will automatically enforce this type throughout the FormControl API, such as email.value, email.valueChanges, email.setValue(...), etc.

Nullability

You might wonder: why does the type of this control include null? This is because the control can become null at any time, by calling reset:

const email = new FormControl('[email protected]');
email.reset();
console.log(email.value); // null

TypeScript will enforce that you always handle the possibility that the control has become null. If you want to make this control non-nullable, you may use the nonNullable option. This will cause the control to reset to its initial value, instead of null:

const email = new FormControl('[email protected]', {nonNullable: true});
email.reset();
console.log(email.value); // [email protected]

To reiterate, this option affects the runtime behavior of your form when .reset() is called, and should be flipped with care.

Specifying an Explicit Type

It is possible to specify the type, instead of relying on inference. Consider a control that is initialized to null. Because the initial value is null, TypeScript will infer FormControl<null>, which is narrower than we want.

const email = new FormControl(null);
email.setValue('[email protected]'); // Error!

To prevent this, we explicitly specify the type as string|null:

const email = new FormControl<string|null>(null);
email.setValue('[email protected]');

FormArray: Dynamic, Homogenous Collections

A FormArray contains an open-ended list of controls. The type parameter corresponds to the type of each inner control:

const names = new FormArray([new FormControl('Alex')]);
names.push(new FormControl('Jess'));

This FormArray will have the inner controls type FormControl<string|null>.

If you want to have multiple different element types inside the array, you must use UntypedFormArray, because TypeScript cannot infer which element type will occur at which position.

FormGroup and FormRecord

Angular provides the FormGroup type for forms with an enumerated set of keys, and a type called FormRecord, for open-ended or dynamic groups.

Partial Values

Consider again a login form:

const login = new FormGroup({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

On any FormGroup, it is possible to disable controls. Any disabled control will not appear in the group's value.

As a consequence, the type of login.value is Partial<{email: string, password: string}>. The Partial in this type means that each member might be undefined.

More specifically, the type of login.value.email is string|undefined, and TypeScript will enforce that you handle the possibly undefined value (if you have strictNullChecks enabled).

If you want to access the value including disabled controls, and thus bypass possible undefined fields, you can use login.getRawValue().

Optional Controls and Dynamic Groups

Some forms have controls that may or may not be present, which can be added and removed at runtime. You can represent these controls using optional fields:

interface LoginForm {
    email: FormControl<string>;
    password?: FormControl<string>;
}

const login = new FormGroup<LoginForm>({
    email: new FormControl('', {nonNullable: true}),
    password: new FormControl('', {nonNullable: true}),
});

login.removeControl('password');

In this form, we explicitly specify the type, which allows us to make the password control optional. TypeScript will enforce that only optional controls can be added or removed.

FormRecord

Some FormGroup usages do not fit the above pattern because the keys are not known ahead of time. The FormRecord class is designed for that case:

const addresses = new FormRecord<FormControl<string|null>>({});
addresses.addControl('Andrew', new FormControl('2340 Folsom St'));

Any control of type string|null can be added to this FormRecord.

If you need a FormGroup that is both dynamic (open-ended) and heterogeneous (the controls are different types), no improved type safety is possible, and you should use UntypedFormGroup.

A FormRecord can also be built with the FormBuilder:

const addresses = fb.record({'Andrew': '2340 Folsom St'});

FormBuilder and NonNullableFormBuilder

The FormBuilder class has been upgraded to support the new types as well, in the same manner as the above examples.

Additionally, an additional builder is available: NonNullableFormBuilder. This type is shorthand for specifying {nonNullable: true} on every control, and can eliminate significant boilerplate from large non-nullable forms. You can access it using the nonNullable property on a FormBuilder:

const fb = new FormBuilder();
const login = fb.nonNullable.group({
    email: '',
    password: '',
});

On the above example, both inner controls will be non-nullable (i.e. nonNullable will be set).

You can also inject it using the name NonNullableFormBuilder.

Last reviewed on Tue May 10 2022

© 2010–2023 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://angular.io/guide/typed-forms