Guide to Typed Reactive Forms in Angular

Davide Passafaro
6 min readOct 10, 2022

--

Every Angular developer, even those just starting out, has surely read multiple times about how important it is to use TypeScript to ensure the maintainability and reliability of their code.

However, until recently, it was impossible to type one of the most important features of this framework: Reactive Forms.

Angular v14 introduces this possibility by bringing some very important changes to existing APIs as well as new features.
Let’s take a look at them!!!

NonNullableOption

The first update concerns the introduction of a property that allows you to ensure that FormControl, FormArray, and FormGroup cannot be set to null when the reset() function is called.

Instead, it will restore the initial value assigned during creation:

const name = new FormControl('Hello', { nonNullable: true });

name.setValue('Ciao');
console.log(name.value); // Ciao

name.reset();
console.log(name.value); // Hello

This new option introduces also some implications regarding typing, which I will delve into later.

For further insights, I invite you to read my dedicated article:

Untyped classes and how to migrate

In order to ensure a safer migration of our projects and/or the possibility of using forms as done up to now, without types, the Angular team has introduced three new Untyped classes.

Untyped classes replace the classes we have used up to now:

La classe FormControl viene sostituita con la classe UntypedFormControl. La classe FormArray viene sostituita con la classe UntypedFormArray. La classe FormGroup viene sostituita con la classe UntypedFormGroup.
Reactive Forms classes and their respective Untyped counterparts

After migrating to Angular v14, here’s the official guide, your Reactive Forms will be converted to use Untyped classes, in order to avoid some unpleasant side effects which I’ll discuss shortly.

Let’s take the following FormGroup as an example:

const myForm = new FormGroup({
name: new FormControl(''),
collection: new FormArray([]),
});

Performing the migration you will obtain an UntypedFormGroup:

const myForm = new UntypedFormGroup({
name: new UntypedFormControl(''),
collection: new UntypedFormArray([]),
});

There is also a new UntypedFormBuilder that we can use as follows:

const ufb = new UntypedFormBuilder();

const myForm = ufb.group({
name: ufb.control(''),
collection: ufb.array([]),
});

This new UntypedFormBuilder supports also the new nonNullable option:

const ufb = new UntypedFormBuilder();

const myForm = ufb.nonNullable.group({
name: ufb.nonNullable.control(''),
collection: ufb.nonNullable.array([]),
});

FormControl

Let’s now see how the main classes have been updated starting from the one that represents the most basic element: FormControl.

Let’s start with this example:

const name = new FormControl('Davide');

Through inference, our FormControl is typed as follows:

const name: FormControl<string | null> = new FormControl('Davide');

The FormControl value is typed as string | null because the reset() function is able to delete the value, effectively setting it to null.

To get around this problem, which is actually a very sensible feature, you just need to use the nonNullable option:

const name: FormControl<string> = new FormControl('Davide', {
nonNullable: true
});

However, be careful not to play too much with the inference because it can offer unwanted effects. Let’s take this case for example:

const name = new FormControl(null);

In this scenario, the type is deduced as follows:

const name: FormControl<null> = new FormControl(null);

The type of the value is deduced as null, which is obviously not what you want, so my advice is to explicitly type each of your Reactive Forms so as not to have any nasty surprises:

const name: FormControl<string | null> = new FormControl(null);

// OR

const name = new FormControl<string | null>(null);

It is precisely because of this mechanism, which has been created now, that Reactive Forms are typed, and the Angular team has opted for a migration with Untyped classes, so that your projects will not suffer regressions and it is possible to carry out the necessary refactors gradually.

FormArray

Now let’s move on to FormArrays. Let’s take the following example:

const myList = new FormArray([
new FormControl('Name')
]);

As a collection of elements, in this case a collection of FormControl values, the resulting type through inference is as follows:

const myList: FormArray<FormControl<string | null>> = new FormArray([
new FormControl('Name')
]);

Attempting to add a new element of a different type into the collection would result in a compile error in our code:

const myList: FormArray<FormControl<string | null>> = new FormArray([
new FormControl('Name')
]);

myList.push(new FormControl(42));
// Error: Type 'number | null' is not assignable to type 'string | null'.

Of course, nothing prevents you from explicitly use a more permissive type to handle similar scenarios:

const myList = new FormArray<FormControl<string | number | null>>([
new FormControl('Name')
]);

myList.push(new FormControl(42));

FormGroup

Finally, here we are with FormGroup, where typing introduces significant changes with various impacts.

Let’s start from a simple example:

const myForm = new FormGroup({
name: new FormControl('Davide'),
collection: new FormArray([new FormControl('Red')]),
});

The FormGroup type is deduced by inference as below:

interface MyFormInterface {
name: FormControl<string | null>;
collection: FormArray<FormControl<string | null>>;
}

const myForm = new FormGroup<MyFormInterface>({
name: new FormControl('Davide'),
collection: new FormArray([new FormControl('Red')]),
});

Since the type associated with the FormGroup has mandatory fields, attempting to add or remove fields would result in an error:

interface MyFormInterface {
name: FormControl<string | null>;
collection: FormArray<FormControl<string | null>>;
}

const myForm = new FormGroup<MyFormInterface>({
name: new FormControl('Davide'),
collection: new FormArray([new FormControl('Red')]),
});

myForm.addControl('surname', new FormControl('Passafaro'));
// Error: Argument of type '"surname"' is not assignable to parameter of type '"name" | "collection"'.

myForm.removeControl('collection');
// Error: Argument of type 'string' is not assignable to parameter of type 'never'.

This therefore forces you to have to manage a more permissive type, for example with an interface with optional properties:

interface MyFormInterface {
name: FormControl<string | null>;
surname?: FormControl<string | null>;
collection?: FormArray<FormControl<string | null>>;
}

const myForm = new FormGroup<MyFormInterface>({
name: new FormControl('Davide'),
collection: new FormArray([new FormControl('Red')]),
});

myForm.addControl('surname', new FormControl('Passafaro'));

myForm.removeControl('collection');

This behavior of FormGroups restricts them to scenarios where the quantity and names of form properties are known in advance.

But what if you need to create a FormGroup dynamically?
Don’t worry, the answer to this question is…

FormRecord

To manage FormGroups whose fields cannot be statically defined, the Angular team has created the new FormRecord class.

This class offers you the ability to insert properties dynamically, a bit like the FormArray class, but acting like a map (or dictionary):

const myForm = new FormRecord<FormControl<string | null>>({
name: new FormControl('Davide')
});

myForm.addControl('surname', new FormControl('Passafaro'));

Similar to what we have already seen for the FormArray class, attempting to add a property that does not respect the type results in an error:

const myForm = new FormRecord<FormControl<string | null>>({
name: new FormControl('Davide')
});

myForm.addControl('surname', new FormControl('Passafaro'));

myForm.addControl('surname', new FormControl(54));
// Error: Argument of type 'FormControl<number | null>' is not assignable to parameter of type 'FormControl<string | null>'.

Here too we can solve with a more permissive type:

const myForm = new FormRecord<FormControl<string | number | null>>({
name: new FormControl('Davide')
});

myForm.addControl('surname', new FormControl('Passafaro'));

myForm.addControl('surname', new FormControl(54));cr{

You can also create this new class using the FormBuilder and the NonNullableFormBuilder, thanks to a brand new record() function:

constructor(fb: FormBuilder) {
const myForm = fb.record({});
}

// AND

constructor(nnfb: NonNullableFormBuilder) {
const myForm = nnfb.record({});
}

This class is very useful for creating Reactive Forms based on a configuration obtained through APIs or from other dynamic processing system, without however giving up the power and stability deriving from the use of the types powered by Typescript:

An untyped FormRecord is effectively an UntypedFormGroup

In conclusion, here are some key considerations

The work carried out by the Angular team aims to strengthen and expand the potential of Reactive Forms without leaving anyone behind.

The introduction of the Untyped classes and the FormRecord demonstrate in fact how the Angular team is aware of having to maintain — in fact — all the thousands of projects created through this framework.

The addition of the nonNullable option is instead a sign of the particular attention to making the development experience easier and safer.

Thanks for reading so far 🙏

I’d like to have your feedback so please leave a comment, clap or follow. 👏

Then, if you really liked it share it among your community, tech bros and whoever you want. And don’t forget to follow me on LinkedIn. 👋😁

--

--

Davide Passafaro

Senior Frontend Engineer 💻📱 | Tech Speaker 🎤 | Angular Rome Community Manager 📣