Skip to content

Instantly share code, notes, and snippets.

@alxhub
Created September 5, 2018 22:50
Show Gist options
  • Save alxhub/4b4b7f7041e2e5ac5974c595cff4e497 to your computer and use it in GitHub Desktop.
Save alxhub/4b4b7f7041e2e5ac5974c595cff4e497 to your computer and use it in GitHub Desktop.
template typecheck design doc v1

Introduction

Angular templates are complex structures which both utilize and produce type information, similarly to classes or functions in TypeScript code. Historically, the type structures of templates were not well checked by previous versions of ngc. A flag fullTemplateTypecheck did engage a type checking mechanism for template code (on which this work is based), but it was never fully implemented and still failed to catch some common cases of errors.

This document describes the design of an Ivy-based type checking mechanism for templates, based on the previous design but augmented to support catching more error cases and restricted to operating under Ivy's assumption of locality.

Goals and non-goals

The goals of the new checker are as follows:

  • Catch the major types of errors in templates:
    1. Type errors in binding expressions within template text
    2. Compositional errors of generically typed components
  • Support inference of # references within templates
  • Support proper type narrowing of expressions within template directives such as NgIf and NgFor in an extensible way
  • Support all TypeScript strictness modifiers (e.g. strict null checks)
  • Support bindings to native HTML DOM elements and custom elements (that have typing info avaiable)

The new checker also has a few non-goals:

  • Type checking is allowed to be O(program) and not require to be O(template)
  • Access to private members from within a template is not supported

Overview

At a high level, the template type checker leverages the TypeScript type checker by creating a modified ts.Program from the original sources, which includes the type structures of templates expressed as TypeScript code. Each component or directive in the original code will have a "type-check block" (TCB) inserted after the class. Any errors reported can be mapped back via a sourcemap-like mechanism to the template text structure which created it.

The generated TCB includes a prelude which sets up a "context" - the implicit receiver of binding expressions. For a component's template, the implicit receiver is an instance of the component. Expressions (such as those in bindings) can be generated as TS code against this implicit receiver.

Frequently component templates make use of sub-components or directives, including bindings to any @Inputs or @Outputs available. When a sub-component is invoked, its type may depend on the inputs passed to it. The modified program thus includes a "type constructor" for each component/directive, which leverages the language's type inference to generate a return type based on the bindings to the component. This type constructor is represented as a static field on the component/directive class, named ngTypeCtor.

When a structural directive is encountered, the TCB generates a new context variable, which acts as the implicit receiver for the expressions within the implicit <ng-template>. To support type narrowing, the statements which represent the <ng-template> bindings can be enclosed in an if block which invokes one or more type guard functions if any are present on the structural directive class. This allows narrowing to work properly.

Detailed Design

Type Check Block structure

A type check block is a single-argument function declared inline in the user's code. Consider a basic component with an empty template:

@Component({
  selector: 'hello-world',
  template: '',
class HelloWorldCmp {
}

This results in the following type check block:

function HelloWorldCmp_TypeCheckBlock(ctx0: HelloWorldCmp) {
}

This function is never invoked; it exists solely for the type checker to process. The only argument sets up a "context" - a scope for future bindings that will act as the implicit receiver.

Interpolation expressions

Now suppose that the template interpolates a name field on the component:

{{name}}

The TCB gains an additional expression statement which validates this interpolated expression:

typescript function HelloWorldCmp_TypeCheckBlock(ctx0: HelloWorldCmp) { ctx0.name; }


### HTML elements and binding expressions

Suppose the template wraps the `{{name}}` interpolation in a `<span>` with a binding to its `title` property:

```html
<span [title]="name">{{name}}</span>

This element is represented as an e0 variable, and the binding to title as an assignment statement:

function HelloWorldCmp_TypeCheckBlock(ctx0: HelloWorldCmp) {
  let e0 = document.createElement('span');
  e0.title = ctx0.name;
  ctx0.name;
}

Unlike ctx0, e0's type is inferred from a document.createElement() call. TypeScript's excellent HTML typings will ensure this call returns an HTMLSpanElement-typed value, which has a title property. The next line validates the binding to title by assigning the value of the binding expression to it. If the types don't match, this will generate an error.

Angular components

Suppose instead of <span> a custom <name-view> component is used:

<name-view [value]="name"></name-view>

The TCB compiler needs to detect that <name-view> is an Angular component and not an HTML element. It can do this because it knows which NgModule the HelloWorldCmp belongs to, and thus knows the set of Angular components usable within that module. Instead of calling document.createElement(), the specific component type is obtained by calling a static ngTypeCtor function on the component class:

function HelloWorldCmp_TypeCheckBlock(ctx0: HelloWorldCmp) {
  let d0 = NameViewCmp.ngTypeCtor({name: ctx0.name});
}

This call actually serves two purposes. Firstly, it allows d0's type to be inferred as NameViewCmp, allowing further type checking. Secondly, it checks the types of all bindings to NameViewCmp's @Inputs. The reason these two operations happen together is that for some components and directives (such as NgFor) the directive instance type is determined from its inputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment