As of Angular 14, reactive forms are strictly typed by default.
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).
Untyped Forms
Non-typed forms are still supported, and will continue to work as before. To use them, you must import the Untyped
symbols from @angular/forms
:
const login = new UntypedFormGroup({ email: new UntypedFormControl(''), password: new UntypedFormControl(''),});
Each Untyped
symbol has exactly the same semantics as in previous Angular version. 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('angularrox@gmail.com');
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('angularrox@gmail.com');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('angularrox@gmail.com', {nonNullable: true});email.reset();console.log(email.value); // angularrox@gmail.com
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('angularrox@gmail.com'); // Error!
To prevent this, we explicitly specify the type as string|null
:
const email = new FormControl<string|null>(null);email.setValue('angularrox@gmail.com');
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
.