In this article, I will dive into Angular type-checking system, show how it’s done in View Engine and also highlight the latest features and improvements which are available in Ivy.

After reading this article you will:

  • learn how Angular type-checks templates
  • see the difference between View Engine (VE) and Ivy type-checking
  • become familiar with new Ivy type-checking features
  • get more confident with template errors since you will know how to find and understand generated type-checking code

Let’s get started.

A little bit of history#

I don’t remember from which Angular version we started getting strange errors in our production build but you should be familiar with some of  them:

Property ‘resisterUser’ does not exist on type ‘LoginRegisterComponent’Expected 0 arguments, but got 1Supplied parameters do not match any signature of call target

It shows us that Angular treats a component’s template as a partial TypeScript file.

But how can that be if templates are html files?

Type Check Block to the rescue#

The answer is simple. Angular emits all binding expressions in a way that can be type-checked. To be more precise, Angular creates Type Check Blocks (TCBs) based on the component template.

Basically, a Type Check Block is a block of TypeScript code which can be inlined into source files, and when type checked by the TypeScript compiler will give us information about any typing errors in  template expressions.

Let’s suppose that we have a simple component:

@Component({
  selector: 'app-root',
  template: '{{ foo }}'
})
export class AppComponent {}

Here’s how the type check block looks like for this component:

var _decl0_1: AppComponent = (<any>(null as any));
function _View_AppComponent_1_0(): void {
  const currVal_0: any = _decl0_1.foo;
}

As you can guess, the code above will fail to compile at this point _decl0_1.foo since the foo property does not exist on AppComponent class.

This is just a concept of how it works. Let’s now look at how it’s handled by View Engine and Ivy.

Type-checking in View Engine#

You probably know that Angular compiler generates factory files for all components and modules. In addition, each NgModule factory file also contains generated type-check  blocks of all components declared in this module. In other words, all module-based factories will contain generated TCBs.

A simple module like:

@NgModule({
  ...
  declarations: [
    AppComponent
  ]
})
export class AppModule {}

will produce synthetic file like:

import * as i0 from '@angular/core';
import * as i1 from './app.module';
import * as i2 from '@angular/common';
import * as i3 from './foo.component';
import * as i4 from './app.component';
import * as i5 from '@angular/platform-browser';
import * as i6 from './foo.module';
export const AppModuleNgFactory:i0.NgModuleFactory<i1.AppModule> = (null as any);
var _decl0_0:i2.NgClass = (<any>(null as any));
var _decl0_1:i2.NgComponentOutlet = (<any>(null as any));
var _decl0_2:i2.NgForOf<any> = (<any>(null as any));
var _decl0_3:i2.NgIf = (<any>(null as any));
// ...
var _decl0_28:i0.TemplateRef<any> = (<any>(null as any));
var _decl0_29:i0.ElementRef<any> = (<any>(null as any));
function _View_AppComponent_Host_1_0():void {
  var _any:any = (null as any);
}
function _View_AppComponent_1_0():void {
  var _any:any = (null as any);
  const currVal_0:any = _decl0_12.title;
  currVal_0;
}

So what’s is currently being checked with fullTemplateTypeCheck enabled? As it turns out quite a few things.

component member access#

In case we forgot to declare some property or method in component Angular’s type-checking system will produce a diagnostic:

{{ unknownProp }}

The produced TCB looks like:

var _decl0_12:i4.AppComponent = (<any>(null as any));
function _View_AppComponent_1_0():void {
  var _any:any = (null as any);
  const currVal_0:any = _decl0_12.unknownProp; //  Property 'unknownProp' does not exist on type 'AppComponent'.
  currVal_0;
}

event bindings#

The compiler will remind us that we forgot to add arguments to the method executed from template

@Component({
  selector: 'app-root',
  template: '<button (click)="test($event)">Test</button>'
})
export class AppComponent {
  test() {}
}

Note that I intentionally left the test method without parameters. If we try to build this component in AOT mode we’ll get the error Expected 0 arguments, but got 1 .

var _decl0_1: AppComponent = (<any>(null as any));
function _View_AppComponent_1_0(): void {
  var _any:any = (null as any);
  const pd_1:any = ((<any>_decl0_1.test(_any)) !== false);
                                   ^^^^^^^^^^
}

HostListener#

Consider the following code in your component:

@HostListener('click', ['$event'])
onClick() {}

The compiler will generate TCB as follows:

function _View_AppComponent_Host_1_0():void {
 var _any:any = (null as any);
 const pd_0:any = ((<any>_decl0_12.onClick(_any)) !== false);
}

So we again get the similar to the error we got in event binding above:

Directive AppComponent, Expected 0 arguments, but got 1.

compiler can understand almost all template expressions like:#

{{ getSomething() }}
{{ obj[prop][subProp] }}
{{ someMethod({foo: 1, bar: '2'}) }}

type-checking for pipe#

The types of the pipe’s value and arguments are matched against the transform() call.

<div>{{"hello" | aPipe}}</div>
// Argument of type "hello" is not assignable to parameter of type number

{{ ('Test' | lowercase).startWith('test') }} 
// error TS2551: Property 'startWith' does not exist on type 'string'. Did you mean 'startsWith'?

type-safety for directives accessed by template reference variable ‘#’#

<div aDir #aDir="aDir">{{aDir.fname}}</div>
Property 'fname' does not exist on type 'ADirective'. Did you mean 'name'?

$any keyword#

We can disable type-checking of a binding expression by surrounding the expression in a call to the $any()

$any(this).missing // ok

So that there shouldn’t be any problem in case we don’t have missing property defined.

non-null type assertion operator#

It’s helpful when we use “strictNullChecks”: true in tsconfig.json

{{ obj!.prop }}

type guard for ngIf#

Let’s say we have added strictNullChecks option in tsconfig.json file and our component contains the following property:

person?: Person;

We can write a template like:

<div *ngIf="person">{{person.name}}</div>

This feature makes it possible to guard person.name access by two different ways:

  1. ngIfTypeGuard wrapper

If we add the following static property to the ngIf directive:

static ngIfTypeGuard: <T>(v: T|null|undefined|false) => v is T;

then the compiler will generate TCB similar to:

if (NgIf.ngIfTypeGuard(instance.person)) {
 instance.person.name
}

The ngIfTypeGuard guard guarantees that instance.person used in the binding expression will never be undefined .

2. Use expression as a guard

By adding the following static property to the ngIf directive:

public static ngIfUseIfTypeGuard: void;

we add more accurate type-checking by allowing a directive to use the  expression passed directly to a property as a guard instead of filtering  the type through a type expression.

if (instance.person) {
  instance.person.name
}

You can read more on this in Angular docs https://angular.io/guide/aot-compiler#type-narrowing

Ivy type-checking#

Remember, in View Engine TCBs are placed in NgModule factories. TypeScript has to re-parse and re-type-check those files when processing the type-checking program.

The new Ivy compiler uses a far more performant approach. It augments the program with a single synthetic __ng_typecheck__.ts file, into which all TCBs are generated.

Additionally, Ivy compiler introduced special kind of methods called type constructors.

A type constructor is a specially shaped TypeScript method that permits  type inference of any generic type parameters of the class from the  types of expressions bound to inputs or outputs, and the types of  elements that match queries performed by the directive. It also catches  any errors in the types of these expressions.

The type constructor is never called at runtime, but is used in type-check blocks to construct directive types.

A type constructor for NgFor directive looks like:

static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>, ‘ngForOf’|’ngForTrackBy’|’ngForTemplate’>>): NgForOf<T>;

A typical usage would be:

NgForOf.ngTypeCtor(init: {ngForOf: [‘foo’, ‘bar’]}); // Infers a type of NgForOf<string>.

Type constructors are also inlined into __ng_typecheck__.ts file.

There are some exceptions when Ivy has to inline TCB blocks into the current processing file:

  • The class component doesn’t have the export modifier
  • The component class has constrained generic types, i.e.
class Comp<T extends { name: string }> {}

But in most cases, you will find all TCBs in __ng_typecheck__.ts file.

Let’s see which improvements in type-checking have been made in Ivy.

Type checking of directive inputs#

It’s now possible to get an error if you’ve passed a property of a wrong type to a directive:

<app-child [prop]="'text'"></app-child>

export class ChildComponent implements OnInit {
  @Input() prop: number;

In VE the generated code looks like:

function _View_AppComponent_1_0():void {
  var _any:any = (null as any);
  const currVal_0:any = 'text';
  currVal_0;
}

Ivy brings us improved TCB:

const _ctor1: (init: Partial<Pick<i1.ChildComponent, "prop">>) => i1.ChildComponent = (null!);

function _tcb1(ctx: i0.AppComponent) {
 if (true) {
   var _t1 = document.createElement("app-child");
   var _t2 = _ctor1({ prop: "text" }); //  error TS2322: Type 'string' is not assignable to type 'number'.
 }
}

Case with an unobvious directive:

<input ngModel [maxlength]="max">

max = 100// error TS2322: Type 'number' is not assignable to type 'string'.
The expected type comes from property 'maxlength' which is declared here on type 'Partial<Pick<MaxLengthValidator, "maxlength">>'

At first glance, there shouldn’t be any errors since we can mess it up with maxLength native element property which takes numbers.

But we’re getting an error because of maxlength input property restriction of MaxLengthValidator directive.

Case with a structural directive:

<div *ngFor="let item of {}"></div>

error TS2322: Type '{}' is not assignable to type 'NgIterable<any>'.

In the preceding code, the structural directive will be expanded to the full form and we will see input property binding [ngForOf]=”{}” which leads to the issue.

Element property bindings#

Ivy now can recognize the type of element where we use property binding. For a template like:

<input type="checkbox" checked={{flag}}>

and property flag declared as flag = true in the component we will get:

function _tcb1(ctx: i0.AppComponent) {
  if (true) {
    var _t1 = document.createElement("input");
    _t1.checked = "" + ctx.checked; // error TS2322: Type 'string' is not assignable to type 'boolean'.
  }
}

Note how compiler defined the element:

var _t1 = document.createElement("input");

Since TypeScript has a mapping from tag names to element type it will result in a type of HTMLInputElement , not simple HtmlElement . Just think how cool it is! We now have typesafety for all props and methods of html elements.

What is even more interesting is that this approach can be extended to  define custom web components. This required CUSTOM_ELEMENTS_SCHEMA  before, but can now leverage full type checking!

In View Engine the TCB block looks like:

function _View_AppComponent_1_0():void {
  var _any:any = (null as any);
  const currVal_0:any = i0.ɵinlineInterpolate(1,'',_decl0_12.flag,'');
  currVal_0;
}

As we can see there is no property assignment at all.

type-safety for any ‘#’ references#

Ivy can understand which directive we’re referring to:

{{x.s}}
<app-child #x></app-child>

TCB:

const _ctor1: (init: Partial<Pick<i1.ChildComponent, "prop">>) => i1.ChildComponent = (null!);
function _tcb1(ctx: i0.AppComponent) {
  if (true) {
    var _t1 = _ctor1({});
    _t1.s; // Property 's' does not exist on type 'ChildComponent'.
    var _t2 = document.createElement("app-child");
  }
}

In addition, Ivy compiler knows exactly the type of element with template reference variable assigned:

{{x.s}}
<input #x type="text">

TCB for this case:

function _tcb1(ctx: i0.AppComponent) {
  if (true) {
    var _t1 = document.createElement("input");
    _t1.s; // Property 's' does not exist on type 'HTMLInputElement'.
  } 
}

Guard for template context ngTemplateContextGuard#

This is one of my favorite features. We can create angTemplateContextGuard static method in a structural directive to keep the correct type of the  context for the template that this directive will render.

The ngTemplateContextGuard method is a user-defined type guard which allows us to narrow down the type of an object within a conditional block.

A widely used NgForOf directive has ngTemplateContextGuard as follows:

static ngTemplateContextGuard<T>(dir: NgForOf<T>, ctx: any): ctx is NgForOfContext<T> {
  return true;
}

and also defines NgForOfContext shape:

export class NgForOfContext<T> {
  constructor(
      public $implicit: T, public ngForOf: NgIterable<T>, public index: number,
      public count: number) {}

  get first(): boolean { return this.index === 0; }

  get last(): boolean { return this.index === this.count - 1; }

  get even(): boolean { return this.index % 2 === 0; }

  get odd(): boolean { return !this.even; }
}

And it actually brings us type-safety for ngFor.

Let’s look at two examples:

Suppose we render a list of names through ngFor:

<div *ngFor="let item of [{ name: '3'}]">
  {{ item.nane }}
</div>

It will produce the following TCB in Ivy:

import * as i0 from './src/app/app.component';
import * as i1 from '@angular/common';

const _ctor1: <T = any>(init: Partial<Pick<i1.NgForOf<T>, "ngForOf" | "ngForTrackBy" | "ngForTemplate">>) => i1.NgForOf<T> = (null!);

function _tcb1(ctx: i0.AppComponent) {
  if (true) {
    var _t1 = _ctor1({ ngForOf: [{ "name": "3" }] });
    var _t2: any = (null!);
    if (i1.NgForOf.ngTemplateContextGuard(_t1, _t2)) {
        var _t3 = _t2.$implicit;
        var _t4 = document.createElement("div");
        "" + _t3.nane;
    }
  }
}

And will give us an error:

error TS2339: Property ‘nane’ does not exist on type ‘{ “name”: string; }’

It works! Magic, right?

Let’s unpack some pieces to understand what’s going on (typescript playground).

TCB for ngFor template
  1. We create _ctor1 function which takes init object as a parameter and returns NgForOf<T> generic type.
  2. This means that once we call that _ctor1we receive NgForOf class of the type we’ve passed to the _ctor1. So we get _t1: NgForOf<{ name: string; }>
  3. We use user-defined type guard where we’re passing two variables _t1 and _t2 declared above.
  4. The goal of theNgForOf.ngTemplateContextGuard generic guard is to narrow the second argument ctx to the NgForOfContext of the generic type we’ve passed to the first argument dir: NgForOf<T> . It’s done by using generic type predicate ctx is NgForOfContext<T>.
NgForOf.ngTemplateContextGuard(_t1, _t2)
                                /     \
      NgForOf<{ name: string; }> =>  NgForOfContext<{name: string;}>

5. It’s now guaranteed that inside if (NgForOf.ngTemplateContextGuard(_t1, _t2)) { scope the _t2 has NgForOfContext<{name: string;}> type. It means that _t2.$implicit returns object of {name: string;} type.

6. The{name: string;} type doesn’t have property ‘nane’ declared.

Another interesting case is here:

<div *ngFor="let item of '3'; let i = 'indix'"></div>

where you will get the error:

error TS2551: Property ‘indix’ does not exist on type ‘NgForOfContext<string>’. Did you mean ‘index’?

since the template will produce the following TCB:

const _ctor1: <T = any>(init: Partial<Pick<i1.NgForOf<T>, "ngForOf" | "ngForTrackBy" | "ngForTemplate">>) => i1.NgForOf<T> = (null!);

function _tcb1(ctx: i0.AppComponent) {
  if (true) {
    var _t1 = _ctor1({ ngForOf: "3" });
    var _t2: any = (null!);
    if (i1.NgForOf.ngTemplateContextGuard(_t1, _t2)) {
        var _t3 = _t2.$implicit;
        var _t4 = _t2.indix; // error TS2551: Property 'indix' does not exist on type 'NgForOfContext<string>'. Did you mean 'index'?
        var _t5 = document.createElement("div");
    }
  }
}

So it restricts names of properties we can use to assign to the local template variable.


In this article we’ve looked at many cases handled by View Engine and Ivy compilers. Let’s recall what is type-checked by Ivy:

  • directive inputs
  • element methods and properties
  • more accurate type-checking for '#' references
  • ngFor context
  • context of ng-template

Now let’s see where we can find this generated code in case you want to mess around on your own.

Exploring generated type-checking code#

Angular  CLI uses webpack under the hood and manages a virtual file system internally. This means no file is saved to disk, they are all saved in memory. All TCBs are generated into synthetic typescript files and hence typescript program can gather diagnostics from them as from any other source file. The Angular compiler reports errors that refer to synthetic files, so interpreting the diagnostics and finding the root cause is quite a challenge.

So, how can we still do it?

The first solution could be debugging Angular CLI node process. Another option is to change the source code in the node_modules folder. But it can be hard for many developers who are not familiar with Angular internals.

There is an alternative hacky way however I use when I want to look at the root cause of the issue in a template. Let’s enable the type-checking feature by editing tsconfig.app.json and add a section of angularComplierOptionand set the enableFulltemplateCheckto true .

"angularCompilerOptions": {
  "fullTemplateTypeCheck": true,
}

Then create a simple js file with any name, for example typecheck.js, in the root folder of your app. We will run this file in NodeJs.

ViewEngine version (Angular CLI 8.1.0) of this file will look like:

const { AngularCompilerPlugin } = require('./node_modules/@ngtools/webpack/src/angular_compiler_plugin.js');

const old = AngularCompilerPlugin.prototype._createOrUpdateProgram;
AngularCompilerPlugin.prototype._createOrUpdateProgram = async function() {
  await old.apply(this, arguments);

  const sourceFile = this._program.tsProgram.getSourceFiles().find(sf => sf.fileName.endsWith('app.module.ngfactory.ts'));
  console.log(sourceFile.text);
};

require('./node_modules/@angular/cli/bin/ng');

Ivy version (Angular CLI 8.1.0 created with -— enable-ivy flag):

const { TypeCheckFile } = require('./node_modules/@angular/compiler-cli/src/ngtsc/typecheck/src/type_check_file.js');

const old = TypeCheckFile.prototype.render;
TypeCheckFile.prototype.render = function() {
  const result = old.apply(this, arguments);

  console.log(result.text);
  return result;
};
require('./node_modules/@angular/cli/bin/ng');

In the code above I’m monkey-patching some internal methods and execute ng command within this context.

Now, all you need to do is to run the following command in your terminal:

node typecheck build --aot

Here I’m executing created above typecheck.js file with build — aot parameters.

Note that we can omit --aot option in Ivy if it’s enabled by default in angular.json file.

Summary#

The type-checking system in Angular is evolving and now Ivy catches lots of typing bugs which VE does not. It opens more and more possibilities to improve it but Ivy is still under active development. For example, there is no source mapping enabled yet  (but there are some attempts to enable it). And there is no type-safety for HostListener yet.

I hope this article clarified a bit what Angular type-checking looks like.