Type-checking templates in Angular ViewEngine and Ivy

Beware what enabling full template type-checking in Ivy will bring us.

Alexey Zuev
Angular In Depth
9 min readJul 9, 2019

--

Original 📷 by Jesse Bowser

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

In this article, I will dive into Angular type-checking system, show how it’s done in ViewEngine 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 ViewEngine (VE) and Ivy type-checking
  • be familiar with new Ivy type-checking features
  • be more confident with template errors since you will know how to see 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 1

Supplied 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?

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:

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

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 look how it’s handled by ViewEngine and Ivy.

Type-checking in View Engine

You should be familiar 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:

will produce synthetic file like:

So what’s is currently being checked with fullTemplateTypeCheck enabled?

# component member access

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

Produced TCB looks like:

# event bindings

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

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 .

# HostListener

Consider the following code in your component:

The compiler will generate TCB as follows:

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:

# type-checking for pipe

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

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

# $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 ViewEngine 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:

Ivy brings us improved TCB:

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:

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 ViewEngine the TCB block looks like:

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:

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:

# 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:

and also defines NgForOfContext shape:

And it actually brings us type-safety for ngFor.

Let’s look at two examples:

Suppose we render a list of names through ngFor:

It will produce the following TCB in Ivy:

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:

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:

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

We’ve looked at many cases handled by ViewEngine 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 go?

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

There is an alternative hacky way 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:

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

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 clarified a bit what Angular type-checking looks like.

Special thanks to Joost Koehoorn and Christian Janker for reviewing the article!

--

--