Angular Kenya Meetup

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. Stephen implemented many of the techniques he describes in this post when working on our Angular Data Grid.

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;

Explicit attribute binding will work:

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

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>

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 | '';

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;
}

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);
    }
}

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>;

And:

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

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;

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);
}

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>

Disable type checking with "$any()":

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

Provide a default value "|| false":

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

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

e.g.

"angularCompilerOptions": { "strictNullInputTypes": false}

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]>();
}

Then:

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

And:

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

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.

Slides

Download pdf of the slides

Talk Recording

Watch On YouTube

AG Grid's Angular Data Grid is available as an MIT Licensed Open Source product which can be freely used in commercial applications, and as a more fully featured version with a commercial enterprise license.