Application upgrade
Angular SPA Typescript Visual Studio Code

How to Use Strict Typing in an Angular Application – Part 2

Welcome to today’s post.

In today’s post I will conclude the two-part post about strict typing in Angular applications by covering some additional discussions including best practices on resolving some additional typing cases. In the previous post I covered the resolution of a variety of common typing issues that are encountered within an Angular application. In addition to what I discussed in the previous post, I will go over the following:

In addition to what I discussed in the previous post, I will go over the following:

  1. When we can use the null-assertion operator in cases where we have null or undefined type conflicts.
  2. When we can use Visual Code’s built-in code linting tools to suggest inferred type fixes and when to fallback and use the any type.
  3. Dealing with Strict Typing when assigning Array types.

Accessing Object Properties using Strict Typing

In an interface or class, we can improve nullity checking on dependent interfaces or classes by using the optional chaining operator (?) or appending the undefined type on the property declaration(s). The source class is shown below:

import { Book } from './Book';

export class TopBooksLoanedReportViewModel
{
    public title: string;
    public ranking: number;
    public count: number
    public averageRating: string;
    public bookDetails: Book;
}

Adding type compliance is achieved as shown:

import { Book } from './Book';

export class TopBooksLoanedReportViewModel
{
    public title?: string;
    public ranking?: number;
    public count?: number
    public averageRating?: string;
    public bookDetails?: Book;
}

Another way of achieving the above is to appending types as shown:

import { Book } from './Book';

export class TopBooksLoanedReportViewModel
{
    public title: string | undefined;
    public ranking: number | undefined;
    public count: number | undefined;
    public averageRating: string | undefined;
    public bookDetails: Book | undefined;
}

When accessing object properties, we then use can use a null assertion as follows:

bookDetails!.title

Allowing External Libraries to be Imported

External libraries with strict mode do not import as they contain implicit types. A typical error is shown below:

Error: error TS7016: Could not find a declaration file for module 'lodash/cloneDeep'
Example:
src/app/services/lookup.service.ts:7:28 - error TS7016: Could not find a declaration file for module 'lodash/cloneDeep'. 'c:/dev/angular-app/node_modules/lodash/cloneDeep.js' implicitly has an 'any' type.

You will see the following recommendation from the compiler:

Try `npm install @types/lodash` if it exists or add a new declaration (.d.ts) file containing `declare module 'lodash/cloneDeep';`
7 import * as cloneDeep from 'lodash/cloneDeep';
                             ~~~~~~~~~~~~~~~~~~

To remediate this error, we will require two tasks:

1.Add the following key value to the TS config options:

"allowSyntheticDefaultImports": true

2.With each import, where you are importing all the types for the library members such as below:

import * as cloneDeep from 'lodash/cloneDeep';

Modify the import to only import the default type for the member as follows:

import cloneDeep from 'lodash/cloneDeep';

A common issue is when we try to use the inferred type based on usage of the type in determining the actual type. It leads to less clean code.

Applying inferred usage doesn’t always give us cleaner code

In the code sample below,

eventSelection(value) {
    this.selectedGenre = value.currentTarget.innerText;
    this.book.genre = value.currentTarget.innerText;
}

we have the value parameter without a declared type giving the error:

Parameter 'value' implicitly has an 'any' type.ts(7006)

If we then use the built-in Quick Fix to correct the type, we will end up with:

eventSelection(value: { currentTarget: { innerText: string; }; }){
    this.selectedGenre = value.currentTarget.innerText;
    this.book.genre = value.currentTarget.innerText;
}

What the inference has done is to expand the type into a JSON object that contains keys and properties that are subsequently used within the method.

In the above case, we can still use the any type as shown below:

eventSelection(value: any){
    this.selectedGenre = value.currentTarget.innerText;
    this.book.genre = value.currentTarget.innerText;
}

and it would still be valid, but not as strictly typed.

We can make use of the Visual Studio IDE to correct many of the above errors.

Making use of the useful Quick Fix … option in the context menu is shown below:

When the above option is selected, we can then decide to either infer the type for the current method or apply it for all types as shown:

The case below is where we just declare the component property without a type as shown:

To fix, we use the Quick Fix … and inferred type to turn it into an any type as shown:

The case below is where we have the suspected null of an object that has properties:

In this case, the Quick Fix … suggests appending an undefined as shown:

Resolving tricky type inferences

We have some code that has an array type that pushes objects that have the following statement:

library!.meetingRooms!.push(cloneDeep(meetingRooms.find(r => 
 	r.libraryId === 5)));

We get the following error on compilation:

Argument of type 'MeetingRoom | undefined' is not assignable to parameter of type 'MeetingRoom'. Type 'undefined' is not assignable to type 'MeetingRoom'.ts(2345)

Inspecting the types of the various components we have:

(alias) cloneDeep<MeetingRoom | undefined>(value: MeetingRoom | undefined): MeetingRoom | undefined
import cloneDeep

and

(method) Array<MeetingRoom>.push(...items: MeetingRoom[]): number

and

(property) Library.meetingRooms: MeetingRoom[] | undefined

As we can see, there is an incompatibility when pushing objects of type MeetingRoom onto an array of objects of type MeetingRoom | undefined.

The declaration of the meetingRooms property of the MeetingRoom interface is:

public meetingRooms: MeetingRoom[] | undefined;

To be able to push objects of type MeetingRoom onto the array, we re-declare the property as:

public meetingRooms: (MeetingRoom | undefined)[] | undefined;

The Visual Studio Code type inspector shows the typing of the push method of the erroneous command as:

(method) Array<MeetingRoom | undefined>.push(...items: (MeetingRoom | undefined)[]): number

We have just matched the typing of the objects being pushed to the typing of the objects of the meetingRooms array property.

Despite fixing the errors the occurred with the static linting, the build still fails with an undefined error. So, we improve the declaration of the meetingRooms property array by removing the undefined from the type and post-fixing the property name with the not-null assertion operator as shown:

public meetingRooms!: (MeetingRoom | undefined)[];

The above now fixes both our linting errors and build type errors.

Dealing with Strict Typing of Arrays

Suppose we have an array declaration such as the one below which covers both null and undefined types:

reviews: BookReview[] | undefined | null = [];

and is used as a subscription response below:

this.bookReviewReport$.pipe(
    map((r: BookReviewReport) => 
    {
        return (r != null) ? r.reviews: null; 
    })
).subscribe((r) => {
    this.reviews = r;
});

With the type casting-error we get below:

src/app/book/book-review/book-review.component.ts:74:7 - error TS2322: Type 'BookReview[] | null | undefined' is not assignable to type 'BookReview[]'.       
  Type 'undefined' is not assignable to type 'BookReview[]'.
74       this.reviews = r;
         ~~~~~~~~~~~~

We can rectify the above with the following changes:

1.Redeclare the array variable as shown:

reviews: BookReview[] = [];

2.Append a null-assertion operator to the right-side object to allow either side to have a null or undefined as shown:

this.reviews = r!;

The above cases have shown us numerous coding fixes for common scenarios that result in strict type compilation errors. We have also seen how to make use of the Quick Fix … context menu to apply inferred type corrections to our untyped properties and parameters within our Angular applications.

To be able to identify and remediate the above errors efficiently requires having us having prior experience and being familiar with union types and use of the various operators such as the null-assertion operator. Provided we can keep the application logic unchanged then the process should be pain free.

That is all for today’s post.

I hope you have found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial