Angular Kenya Meetup

Alan Richardson - Jun 30 '22 - - Dev Community

Stephen Cooper presented at the Angular Kenya Meetup on June 23 2022. Explaining lessons learned from building components that are used by other people when they have template type checking enabled.

This allows IDEs to warn about type errors as we are coding to get early pre-compile time warnings.

With strict mode enabled by default, we must acknowledge that there is nothing more tiresome than using a component that is poorly typed. As we develop our components we must consider the implications of our Input types for developers that have strict mode enabled.

Just defining the Input types isn't enough to avoid type failures.

If you want to support shorthand boolean attributes then a declaration can't just be boolean.

public disabled: boolean = false;

Enter fullscreen mode Exit fullscreen mode

Explicit attribute binding will work:

<app-display [disabled]="true"></app-display>

Enter fullscreen mode Exit fullscreen mode

But when someone uses a plain attribute, it doesn't work because the default is equivalent to an empty string.

<app-display disabled></app-display>

Enter fullscreen mode Exit fullscreen mode

This would generate a visual warning in the IDE because the types don't match and a "Type 'string' is not assignable to type 'boolean'."

There are two approaches to handling this depending upon the Angular/Typescript version.

In Angular v9-14 we can use ngAcceptInputType_ for Input Coercion, or we use Set and Get with different types for Typescript v4.3 and Angular v13+.

ngAcceptInputType_ is a static property supported by the Angular compiler that enables you to widen the accepted types of an input property.

The input is defined the normal way, but we also add a static ngAcceptInputType_ property to allow the compiler to accept multiple types.

@Input()
public disabled: boolean = false;
static ngAcceptInputType_disabled: boolean | '';

Enter fullscreen mode Exit fullscreen mode

But we still have to convert the empty string to true, using ngOnchanges:

ngOnChanges (changes: SimpleChanges) {
   if (changes.disabled) {
       this. disabled = toBoolean(changes.disabled.currentValue);
   }
}

toBoolean (value: boolean | string) {
    this.disabled = (value === '') || value === true;
}

Enter fullscreen mode Exit fullscreen mode

This can be done for other types as well, e.g. Dates:

@Input ()
public date: Date;
static ngAcceptInputType_date: Date | string;

ngOnChanges (changes: SimpleChanges) {
    if (changes.date) {
        this.date = toDate(changes.date.currentValue);
    }
}

Enter fullscreen mode Exit fullscreen mode

However, will Inputs be set with the async pipe? Do you handle the initial null values?

e.g. if the user is setting the disabled value from an Observable stream

disabledStream$: Observable<boolean>;

Enter fullscreen mode Exit fullscreen mode

And:

<app-display [disabled]="disabledStream$ | async"></app-display>

Enter fullscreen mode Exit fullscreen mode

An Async pipe returns null when no values have been emitted yet.

We would amend the ngAcceptInputType_ to handle null.

static ngAcceptInputType_disabled: boolean | '' | null;

Enter fullscreen mode Exit fullscreen mode

With Typescript 4.3 we can now use Get and Set with different types.

_disabled: boolean = false;

@Input()
get disabled(): boolean{
   return this._disabled;
}

set disabled(value: boolean | string | null){
   this._disabled = toBoolean(value);
}

Enter fullscreen mode Exit fullscreen mode

With no need any more for ngAcceptInputType_.

We can also handle compilation failures for 3rd party components in our code:

Non null assertions with "!":

<app-display [disabled]="(disabledStream$ | async)!"></app-display>

Enter fullscreen mode Exit fullscreen mode

Disable type checking with "$any()":

<app-display [disabled]="$any(disabledStream$ | async)"></app-display>

Enter fullscreen mode Exit fullscreen mode

Provide a default value "|| false":

<app-display [disabled]="(disabledStream$ | async) || false"></app-display>

Enter fullscreen mode Exit fullscreen mode

It is also possible to configure the Angular Compiler options to switch off various checks.

e.g.

"angularCompilerOptions": { "strictNullInputTypes": false}

Enter fullscreen mode Exit fullscreen mode

These are detailed in the Angular documentation - Template Type Checking.

The talk recording demonstrates the use cases for ngAcceptInputType and Set/Get, along with Input coercion, drawing from the experience of preparing the "ag-grid-angular" for use in strict applications.

Additionally the talk goes further and explains the user of generics.

export class GenericComponent<TData>{
    @Input()
    rowData: TData[] | undefined;

    @Output ()
    rowDataUpdated = new EventEmitter<TData]>();
}

Enter fullscreen mode Exit fullscreen mode

Then:

rowData: number[] = [];
onRowDataUpdate (event: string[]) {}

Enter fullscreen mode Exit fullscreen mode

And:

<app-generic
    [rowData]="rowData"
    (rowDataUpdated)="onRowDataUpdate($event)">
</app-generic>

Enter fullscreen mode Exit fullscreen mode

As Inputs are a fundamental part of Angular this will impact every developer once their application enables strict mode. I would hate for developers to resort to any when there are Angular features to maintain fully typed components.

There is also an extended Q&A Session in the video.

Code

Code examples for writing Angular Components that are compliant with Typescript strict mode. Two sample apps for the approaches described under Template Type Checking.

In both demos you will see compilation errors when running npm run start as Stephen has left examples that need fixing following the approaches outlined.

Related Reading

Slides

Angular Kenya Meetup

Download pdf of the slides

Talk Recording

Watch On YouTube

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .