Guide for Type-safe Angular
This is my talk I presented at bitbank LT Night, in Tokyo.
This talk is about my practice around Angular and strict-mode TypeScript. The strict-mode allows us to write much safe and robust application. Honestly, I think all Angular application should be written in strict-mode, but it is a little hard to understand every error messages caused by the compiler.
Imagine a small Angular application
At first, imagine a simple small Angular application that displays a user which is fetched from a remote server.
This application is written in TypeScript. But it isn’t type-safe. I’ll rewrite it in strict-mode TypeScript.
Angular w/ Strict-mode TypeScript
To enable strict-mode, just add strict: true
in your tsconfig.json file. It enables all flags which makes its compiler more strict.
Then I hit a new compile error. Mistakes are two; one is this.user is typed just null at initiation. Another is using http.get without a type parameter.
Problems of Property Initiation
This is the essence of this my talk. Around property initiation in strict-mode, some considerations and decision making are needed.
At the UserDisplayComponent, user input property has an error about property initialization. Despite it is not initiated, its type doesn’t accept undefined.
There are two options to solve this error. One is doing initialization, and another is accepting undefined. This problem was mentioned in a blog by Stephen Fluin, “Property has no initializer and is not definitely assigned”.
What I have to consider is who checks the user property is/isn’t empty.
My practice
In summary, I like to declare input properties as T | null
. In other word, null-checking of user
is a responsibility of UserDisplayComponent.
There are some reasons why I like this approach.
Reactive pattern friendly
The reactive pattern is based on async pipe and Observable properties. A component has some Observables which give data to its template only when the data changed. This pattern is performant and easy to separate concerns about state managing.
But there is a pitfall. Multiple async pipes for the same Observable is a common anti-pattern. It can happen unnecessary change detection or other bugs. To avoid this, typically “ngIf as” syntax is used. This pattern can declare a new variable which is assigned to the ngIf-evaluated value.
It seems good, but actually not good. There is a big problem. We can not separate views by user’s state; fetching or empty. The above template cannot have a fetching view.
Meaning of null: fetching or empty
In common cases, asynchronous data has three states at least: empty, fetching and fetched. So it requires to separate meaning of null of the user.
The below is for example. I call this pattern “single state stream pattern”. user
and userFetching
are in the same state and the same stream. And its template subscribes state$
property to map user’s state into view.
Subscribe the single state stream at the root of the component. The top ngIf evaluates state$
and checks its truthy-ness. The result is false before the initialization but after that always true. So re-rendering doesn’t happen at the root.
The important thing is, Fetching is a container’s state, and Empty is a presenter’s state. Handling fetching state is not the responsibility of UserDisplayComponent because UserDisplayComponent doesn’t know how the application gets the data, sync or async. At the same time, AppComponent doesn’t have to know currently the user is null.
As the result, UserDisplayComponent has to handle two cases.
- The user is null : Empty state (not fetching)
- The user isn’t null: Show user view
Mandatory Inputs
I could choose another choice; UserDisplayComponent takes user input as mandatory. Then, for letting the compiler know the property is always not null, use !
non-null assertions. Non-null assertion is only for TypeScript world, so for keeping safety and confirming inputted value is really non-null, runtime checking is important.
Summary: my practice
- Inputs should be declared as
T | null
. - Create the single state stream and subscribe it at the root of the template.
- Fetching is container’s state, and Empty is presenter’s state.
- Non-null assertions should be used with runtime assertion.
I’m not sure this is the best practice. Please let me know your feeling. Thanks.