Angular 14 was an exciting release because it finally brought out Typed Forms. But how do we go about using them? In this article, I’ll show you exactly how to do it along with some tips and tricks to make your form code better.

So let’s get started!

Introduction

So first, I’ll refer you to the official introduction video by Angular team, in case you want to know the basics of Angular Typed Forms.

In short though, Typed forms means each of your controls will have some specific types – which will be enforced through typescript at compile time – saving us from getting nasty bugs at runtime.

But we’ll see all of that soon. So let’s first open up our existing app. This is the same app we created as part of the Angular Sign up with Firebase series.

It has three reactive forms (login, sign up and profile) – which we can use to test the upgrade to Angular Typed forms.

Upgrading to Angular 14

So to do that, we’ll first update our app to the latest Angular 14 version. We’ll go to update.angular.io and follow the instructions to upgrade from Angular 13 to 14. We’ll also check the Material option – because we’re using the material components.

Now let’s run our app to see if it works.

Great, the update worked.

But let’s go into our code now and see what the update actually did. So as you can see, to ensure backwards compatibility, the migration process converted the FormGroup and FormControls to be Untyped for the time being. This is so our app keeps working smoothly.

loginForm = new UntypedFormGroup({
    email: new UntypedFormControl('', [Validators.required, Validators.email]),
    password: new UntypedFormControl('', Validators.required),
  });

We can then convert it to Typed Forms whenever we want.

What are Untyped Forms?

Let’s first see what we mean by Untyped here. Going into e.g. the login form first, you’ll see that when we try to access the value of the loginForm, it doesn’t show us the controls within it.

Untyped Form Group Values

If we try to access one control, we don’t actually know it’s type. As a result, we can actually assign a wrong type of the value to the control and we won’t know anything till runtime.

No TS error on Untyped Forms setValue

Typed Forms – the difference

So let’s try updating the Untyped Form Group to FormGroup and Untyped FormControl to FormControl. These are the typed versions in Angular 14.

loginForm = new FormGroup({
    email: new FormControl('', [Validators.required, Validators.email]),
    password: new FormControl('', [Validators.required])
 });

Great, so let’s check the type of the control by hovering over the email control.

And as you can see above, it shows the type as a FormControl having a value of string or null. So how did it figure out what type to give this control? This is derived from the initial value that we gave here – which is an empty string.

Now we can also give an explicit type here with the FormControl, but in most cases you won’t need this and it’s actually pretty handy! So we have our typed forms ready now.

Now let’s check out the value of our typed form group and see what we get. So let’s write loginForm.value and you’ll see here that we get our two FormControls inside of it.

And when we try to access a control’s value, we can see that it has the correct type of string along with “undefined” and “null”.

We’ll come to undefined and null in a bit and why they’re here.

But let’s first try to set a value on the form group and see what happens. So if we try to now set a value for the email with a number value, it won’t allow us because it should be a string!

Great, and we can get this error even before compilation. So it makes your forms more resilient and provides a good check that you’re assigning and using values correctly.

But we still have an apparent issue and that is why do we see a null and undefined in the types – when we clearly set an initial value of empty string. Well, these two types are there for different reasons.

Why is there “null” in the types?

Let’s first consider null. So null is there because a form or a control can be reset. For example, if we use loginForm.reset and give no parameters – it is going to reset the state of the form to the DEFAULT value, which is null.

This means that every control’s value can be null at some point. But in our case, this doesn’t make sense. Because our initial value will be an empty string – so we don’t really want reset to value of null – instead we’d like it to be an empty string.

We can do this with a flag called nonNullable. So let’s add that flag to our form controls and we can immediately now see that the type no longer contains null. Great, this is exactly what we needed!

loginForm = new FormGroup({
    email: new FormControl('', { validators: [Validators.required, Validators.email], nonNullable: true} ),
    password: new FormControl('', { validators: [Validators.required], nonNullable: true})
  });

You’ll notice though that adding nonNullable to each of your form controls can get a bit repetitive. So let’s clean this up by using a NonNullable Form Builder.

A form builder is a service which allows us to use a short hand syntax to specify forms.

Angular also provides a nonNullable version of this which will cause all of our controls to be nonNullable by default!

So we’ll inject nonNullableFormBuilder in our component and then use this.fb.group and then specify our controls as before. Notice how this is much cleaner than how we did it previously!

loginForm = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', Validators.required],
  });

Why is there “undefined” in the types?

Now that that is done, let’s look at the undefined type and why we get it in our form group’s value. This is simply because we can remove a control from a form group and also disable it during the working of our app. And if that happens, the value of the form control will become undefined. So Angular has added undefined as a possible type to the value of any control in a form group.

And it is because of that type, that we can see a small red error in the submit handler.

Now our login function here excepts a string value, but Angular says that the email is of type string or undefined. Now since these don’t match, we get this error.

Adding extra checks for undefined values

So how do we fix this?

Well, my way is that I add a specific check for the value of the email and the password just like I do with the form validity. Once I add the email and password to the check, initially, the error goes away because till the point where we call login, the email and password will definitely not be undefined.

submit() {
    const { email, password } = this.loginForm.value;

    if (!this.loginForm.valid || !email || !password) {
      return;
    }

    this.authService
      .login(email, password)
....
}

Great, so as you can see Angular Typed Forms sort of forces us to add a bit of extra checks to our apps to handle all of the edge cases that our forms can have.

Now you may consider it an extra overhead and if you do, then you can easily switch back to the Untyped forms. But if you want to make your app more resilient to runtime errors, typed forms will nudge you in the right direction at development time.

Conclusion

And what about my app? Well, this is all I did. The login works ok now with no TS errors. Moving on to the profile form and the sign up form, I just made the same changes and fixed any red errors I got using the same methods. And viola, my app is now upgraded to Angular typed forms!

In case you want to check out the upgraded app yourself, here is the github link.

https://github.com/thisiszoaib/angular-sign-up

I hope you found this short guide useful and make full use of Typed Forms to build awesome Angular apps. Thanks for reading!

Bye 🙂


Support me!

If you enjoyed this or any of my other posts, you can support me by buying me a coffee below. Thanks in advance!