Using precise types with Typescript

Typescript has a lot to offer when it comes down to… types. Which makes perfect sense, right? Let’s take a few examples based on the following interface:

The following variable is meant to store the id of a User in our application. The type of that id is a number, so this declaration is accurate:

But could be a lot more precise if we used the following syntax instead:

Now we are expressing that id is of the same type as User id (which is still a number), and we make it explicit that we’re using this variable to store a User id, not just any number. We are using an indexed type.

We can use a similar approach for our language. Instead of being any string, we can narrow that type using union types:

A user’s language can only be one of the four listed values. This makes our code safer (a typo would be caught by our compiler and IDE instantly) and doesn’t impact performance (union types do not get compiled into anything).

Using services to cache data

Yesterday, we saw that services are singletons that are created once for all and stay in memory as long as the application is open.

This makes our services the best place to store data that can be shared between multiple different components. Such services can use BehaviorSubjects or Signals to cache that data and replay the last value automatically to any new subscriber (component, directive, pipe, service, etc.).

Here is an example using a signal to store a value and expose it in a read-only fashion:

This article compares BehaviorSubjects and Signals to cache a shareable value.

Lifecycle of Angular applications

The lifecycle of an Angular application is something that many aspiring Angular developers struggle with. People often ask me questions such as:

  • How long does this component/service/directive stay in memory?
  • How do I save the data before I navigate to the next page/view/component?
  • What happens if I open that same app in another browser tab?

Here is how to think about it:

  • When we open an app in a browser tab, we’re booting an Angular application in a self-contained memory space, similar to a virtual machine.
  • Closing that tab is equivalent to killing the application, freeing any memory associated with it, just like when you close a desktop app in your machine’s operating system.

In Google Chrome, there’s even a task manager where you can see the memory footprint and CPU usage of your tabs and browser extensions – they’re just like independent desktop apps:

From an Angular perspective, a component gets loaded in memory whenever displayed on the screen. That’s when its class is instantiated.

Suppose the component gets removed from the screen (by navigating to a different component or removing it with a ngIf directive, for instance). In that case, the component is destroyed, and all of its memory state is gone. The same goes for directives and pipes: They get created when used by a component template and destroyed when that component gets destroyed.

Services are different, though. A service is created by Angular when a component needs it for the first time. Then, that instance remains unique and shared between all components that inject such service. A service doesn’t get destroyed: It remains in memory as long as your app is open and you don’t close your browser or tab.

This answers our three initial questions:

  • How long does this component/service/directive stay in memory?
    Components stay as long as they’re in the DOM. Services stay as long as the app is open.
  • How do I save the data before I navigate to the next page/view/component?
    Not if you save it in a service
  • What happens if I open that same app in another browser tab?
    You create another separate instance of everything: Components, services, etc. Both instances are independent and do not share any data or memory space.

Using setters for better @Input

When using @Input in our components, we can implement ngOnChanges to be notified whenever an input value has changed:

The above code works and would run whenever id or name are updated by the parent component. The problem is that this syntax can quickly become heavy when there are several @Inputs in your component.

Instead, we can do this:

Instead of using @Input() on a class property, we can use it on an ES6 setter function. The benefit of the setter syntax is that it makes it obvious what kind of code we’re running when the @Input value is set. No more ngOnChanges needed.

Build size budgets

As mentioned earlier in this newsletter, the size of your Javascript matters a lot, as our code has to be downloaded first, then parsed and interpreted by a browser, which will get slower and slower as the size of your app increases. This is why we want optimized production builds. And this is also why it’s always a good idea to keep track of the size of your production code after each build.

Fortunately, the Angular team has our back and integrated build budgets in the Angular CLI. You can use those budget settings to decide when to get a warning or even fail a build it your code becomes too big. This configuration happens in angular.json:

The above (default) budgets would warn you if your Javascript exceeds 500Kb in size and fail once the build exceeds 1Mb. Those are already part of your projects, so you don’t have to do anything to use them.

If you do continuous integration, your build would fail right after the commit that degraded your bundle size, making it easy to troubleshoot and fix the issue.

Most of the time, dependencies are the culprit. I remember coaching a client that needed some Excel-like features in the browser, and their build exploded to over 25MB because of a massive monolithic Javascript Excel library… Thanks to the build error, we knew that this library wouldn’t work so we chose a lighter one instead.

In the past, I’ve also inherited projects where I would track our build size version after version. My client was amazed to see that despite adding features, the build was getting smaller and smaller after each release, thanks to Angular always being better at tree-shaking and having the incentive to clean up old code and make it smaller. When you start tracking a metric, you want to improve it!

Use functions for readable RxJs operator chains

When you have complex RxJs operator chains, a good idea is to refactor them into simple functions. This can also favor code reuse for simple scenarios such as the following example.

Say we have a User object with the following shape:

We receive that object through an Observable; all we want from it is the user’s age. Using Rxjs (and date-fns), we could do something like this:

This works, but it’s not instantly apparent that we’re computing the age of the user, and if we need similar code elsewhere, we’d have to copy-paste that line of code, which is terrible.

Instead, we could create a function with an explicit name:

And then use it as our new custom operator function:

We can improve the signature of our function for type safety purposes, too:

You can see that code in action on Stackblitz here.

Obviously, the approach makes even more sense when you have several operators, and you can even combine such operator functions with other operator functions:

Now we have beautiful, maintainable code that can be read without documentation.

Angular style guide

People often ask me the best way to organize their files/folders. While it’s mostly a matter of personal preference or a team decision, the recommendations of the Angular style guide are always good to follow.

For instance, when it comes down to folders, here’s what the official style guide says (the highlight is mine, as I find this to be true for myself):

What’s great about the Angular style guide is that it always provides why such approaches are recommended. As you can see, the above justifications make perfect sense.

Also, while some recommendations are purely naming conventions, others touch on architecture decisions, such as talking to the server through a service or using directives to enhance an element (somewhat similar to my earlier call to think more about directives instead of components).

ngFor trackBy for improved performance

We all use the ngFor directive on a daily basis. The directive is used to repeat a template as many times as needed based on an array of data we want to render. We covered how the directive exposes useful local variables in the past.

To help Angular perform DOM updates when items in the array are reordered, new items are added, or existing items are removed, we can add a trackBy attribute that specifies what unicity criteria Angular will use to track these additions/removals/reorderings:

Of course, this only helps if the array in question is subject to changes in the future. By default, Angular uses Object.is() to compare values in the array, which is somewhat similar to a === comparison.

In our case, we created a function that specifies how to track by _id:

That way, Angular will know how to compare objects efficiently and won’t perform DOM updates that aren’t needed, which can be a huge performance boost if you’re displaying a massive data table with thousands of records. You can see this example in action on Stackblitz here.

Prefer template-driven forms

International readers might not know that today is a public holiday in the United States as the country celebrates its birth and Independence. As a result, I’ll make today’s post very short but still useful.

Over the past couple of newsletters, I explained that template-driven forms are perfectly fine in most cases. A few years ago at ng-conf 2021, Ward Bell said he always uses template-driven forms and explained how he does that in less than 19 minutes.

If you’d rather read a short article, you can head to my “reactive template-driven forms” post where I showcase how template-driven forms are actually… a layer on top of reactive forms, and that you could still access reactive features on a template-driven form if you wanted to. I know, it’s mind-blowing!

Happy 4th of July if you’re in the US!

Reactive forms Observables

Last week, I gave you a simple decision framework to decide when to use reactive or template-driven forms.

As a matter of fact, the only reactive (meaning RxJs-friendly) piece of reactive forms is that FormGroup, FormArray, and FormControl all extend the AbstractControl class and, as a result, expose two different Observables:

Those two can be very powerful when used alongside some RxJs wizardry (see this tutorial on dynamic filtering with RxJs and Angular forms, for example). In their most simple form, we can use those to listen to changes in our form and react accordingly:

In the above example, we enable the city form control if a 5-digit zip code has been entered. Since we are subscribed to valueChanges, any future change of the zipcode value could enable/disable the city input.

The other Observable, statusChanges, focuses on validity changes only and returns one of the four following values:

Here is an example use case for statusChanges, which is more accurate than the previous example as this one would take into account all validation code relevant to the zip code: