Skip to content

First steps towards template type-checking in ngtsc - #26203

Closed
alxhub wants to merge 13 commits into
angular:masterfrom
alxhub:ngtsc-typechecking-start
Closed

alxhub wants to merge 13 commits into
angular:masterfrom
alxhub:ngtsc-typechecking-start

Conversation

@alxhub

@alxhub alxhub commented Oct 1, 2018 •

Copy link
Copy Markdown
Member

This extra-large PR implements major components of template type-checking in ngtsc. Broadly, it includes:

  • Refactors to prepare for the work
  • A template binding API to extract semantic information about templates
  • A type-checking code generator
  • Integration of the above into ngtsc via a second ts.Program created for the purposes of type checking.
  • Tests for the above

The work is split into multiple commits with a narrow enough scope to be reviewed separately. Relevant tests were verified to pass at each commit.

@mary-poppins

Copy link
Copy Markdown

You can preview d1a608d at https://pr26203-d1a608d.ngbuilds.io/.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not isComponent?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There does not seem to be an assertion. If you checking that it does not throw, say so.

@alxhub alxhub Oct 2, 2018 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

driveMain asserts that the compiler ran successfully. This pattern is used in all the tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantic for NonNullable doesn't seem right for NgIf, should be some kind of NonFalsy instead, like:

type Falsy = false | null | undefined | 0 | ''

class NgIf {
  static ngTemplateGuard_ngIf<E>(dir: NgIf, expr: E | Falsy): expr is E
  // or
  static ngTemplateGuard_ngIf<E>(dir: NgIf, expr: E): expr is Exclude<E, Falsy>
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree in principle. For the real NgIf implementation, we may do something like this. However, the important case is to capture the narrowing to NonNullable, because that supports strictNullChecks in the generated type-checking code.

Narrowing other false-y values is useful only in very specific situations (calling methods which only accept truthy types). I expect this to be almost non-existent in practice.

BTW, only your latter version of ngTemplateGuard_ngIf will actually work. The former works well for inferred values of E, but fails if passed an expr with an explicitly nullable type.

@trotyl trotyl Oct 2, 2018 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mechanism doesn't seem to be general enough, how could NgIf guard injected template, ngIfThen and ngIfElse simultaneously? Should at least be possible to write:

class NgIf {
  static ngTemplateGuard_ngIf<E>(...)
  static ngTemplateGuard_ngIf_ngIfThen<E>(...)
  static ngTemplateGuard_ngIf_ngIfElse<E>(...)
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, you can narrow multiple expressions:

class NgIf {
  static ngTemplateGuard_ngIf<E>(...)
  static ngTemplateGuard_ngIfThen<E>(...)
  static ngTemplateGuard_ngIfElse<E>(...)
}

However in practice this would be useless. Narrowing inside a template scope only happens for the <ng-template> that the directive is on. There is no association between NgIf and any other templates (even if they're passed in as input values to ngIfElse, etc).

Such an association would be difficult to build into the system.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, you can narrow multiple expressions

The problem is not about multiple expressions, but multiple <ng-template>:

<!-- items: number[] | null -->
<ng-container *ngIf="items then foo else bar"></ng-container>
<ng-template #foo>{{ items.length }}<!-- No error here --></ng-template>
<ng-template #bar>{{ items.length }}<!-- Throw error here --></ng-template>

And to support that there need to be type guard per @Input() and per TemplateRef, the pattern above I suggested is:

static ngTemplateGuard_(propertyNameForExpressionToNarrowWith)_(proprtyNameForTemplateRef)

There is no association between NgIf and any other templates

The association were built when assignment happens, so the type-checking can happens there as well.

Such an association would be difficult to build into the system.

I agree it require some extra work so may not be added in this PR, but it's still quite possible. The basic model is to construct an anonymous type according to actual usage:

type anonymous_0 = { items: { length: unknown } }

And when the assignment happens check whether the actual type is assignable to usage type, I have made an informative example here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct - your example would not be type-checked at all in the current design. This is a non-trivial problem to solve.

Is anonymous_0 generated from the expressions within the template itself? That's an interesting idea. Off the top of my head it seems very limited and error prone, not to mention exceedingly complicated to generate in the first place.

I like your approach of having additional guard functions for property bindings that happen to take templates. With that approach, even local usage of ngTemplateOutlet could be correctly inferred. The challenge is that the location an <ng-template> is declared can be very different from where it's passed into an input to a directive. For example:

<div *ngIf="blah">
  <!-- deep inside some other structure, we use ngIf -->
  <ng-container *ngIf="items then foo else bar"></ng-container>
</div>

<!-- somewhere up the tree, the templates are actually declared -->
<ng-template #foo>{{ items.length }}<!-- No error here --></ng-template>
<ng-template #bar>{{ items.length }}<!-- Throw error here --></ng-template>

This is totally legal, and tricky to handle - the guards you want around the template TCBs aren't available at the location in the template where they're defined.

What I think we'd have to do is have static methods with two dimensions like you suggest. When evaluating the *ngIf directive on the <ng-container> above, we'd notice that template references are passed to the directive and that narrowing applies. We would assume at that point that the directive intends to render the template, and move the template's TCB inside the appropriate guard, as if it were written at that location anyway.

I think this is a very doable solution, but it's not going to be a priority for now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made a more expressive example of the nesting case above (need to turn on strictNullChecks).

The idea of this is being a side-path for checking the assignment of TemplateRef to @Input(), which is unrelated to the real template instruction, renamed the template functions to _type_checking.

Due to the static nature of context variable, compiler could understand which context it's the variable belongs (e.g. nextContext(1)), so we'll always have a maximum type to be the declared context type. Given the derived type of expressions from nextContext(N), so inside the template we can determine that real expression type *_Expr_NextContext_N to be the intersection of these two:

type App_Context = { blah: ({ items: number[] | null }) | null }
type Container_Expr_NextContext_1 = { blah: { items: unknown } } & App_Context

This approach should be independent of templates nesting as error always happens on input assignment, not template itself. And conceptually being canonical, could even handle the case of same template being used in several places.

(I guess the idea being more close to Flow rather than TypeScript?)

Also I totally agree this is not an easy work and not of a high priority.

(Also TypeScript seems to have bug for { items: unknown } & false not narrow to never)

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from d1a608d to e25a598 Compare October 2, 2018 14:58
@mary-poppins

Copy link
Copy Markdown

You can preview e25a598 at https://pr26203-e25a598.ngbuilds.io/.

@JoostK JoostK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome! 🎉

At some point in the future, might it become possible for component inputs to specified as required? I know you can extend the directive's selector to include the required inputs as attribute selectors, but its ergonomics are far from ideal.

Comment thread packages/compiler/src/selector.ts Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the param optional, now we have to cheat here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's the original API - we can refactor this more in the future. In general, this cast is marking the expectation that callbackCtxt fits into T. If they're using the optional parameter, then T should be |undefined.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incomplete comment?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incomplete comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@JoostK JoostK Oct 2, 2018 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's missing, its value can be seen as being undefined in which case we should use false as type guard?

EDIT Hmm, depends on the directive's own (default) value of course.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we just exclude the guard from the list. Not having a matching guard function just means no narrowing of that input expression takes place.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If no directive guards, why generate if (true) { ... }, where just { ... } would suffice to create a new block?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reason. This way all the blocks get generated with the same structure.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

incomplete comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N is not used?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did TS not infer non-nullable node.name here? It is guarded at the top.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Fixed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is currently not tested if the cached SF was already transformed, is it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good catch - fixed.

Comment thread packages/compiler-cli/test/ngtsc/env.ts Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not JSON.stringify this whole object literal?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason - this was moved code, I didn't modify the semantics.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from e25a598 to fab939e Compare October 2, 2018 18:14
@mary-poppins

Copy link
Copy Markdown

You can preview fab939e at https://pr26203-fab939e.ngbuilds.io/.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from fab939e to ca2d072 Compare October 2, 2018 18:34
@mary-poppins

Copy link
Copy Markdown

You can preview ca2d072 at https://pr26203-ca2d072.ngbuilds.io/.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from ca2d072 to 702bebe Compare October 2, 2018 19:19

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alxhub whoops, this is very much broken now ;)

Instead of a set, I would insert a cache entry such as { transformed: false, sourceFile: sourceFile } into the sfCache instead. Seems more efficient, I'd expect.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh, of course it is. That's what I get for going too fast.

Fixed for real this time.

@mary-poppins

Copy link
Copy Markdown

You can preview 702bebe at https://pr26203-702bebe.ngbuilds.io/.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from 702bebe to 57c5c2d Compare October 2, 2018 22:29
@mary-poppins

Copy link
Copy Markdown

You can preview 57c5c2d at https://pr26203-57c5c2d.ngbuilds.io/.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch 3 times, most recently from ea8c942 to 3b36369 Compare October 3, 2018 20:40
@mary-poppins

Copy link
Copy Markdown

You can preview 3b36369 at https://pr26203-3b36369.ngbuilds.io/.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from 3b36369 to 15f851c Compare October 3, 2018 21:14
@mary-poppins

Copy link
Copy Markdown

You can preview 15f851c at https://pr26203-15f851c.ngbuilds.io/.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from 15f851c to e28e2e4 Compare October 3, 2018 21:27
@alxhub alxhub added the target: major This PR is targeted for the next major release label Oct 3, 2018
@mary-poppins

Copy link
Copy Markdown

You can preview e28e2e4 at https://pr26203-e28e2e4.ngbuilds.io/.

@alxhub
alxhub force-pushed the ngtsc-typechecking-start branch from e28e2e4 to 96b406b Compare October 3, 2018 22:12
@mary-poppins

Copy link
Copy Markdown

You can preview 96b406b at https://pr26203-96b406b.ngbuilds.io/.

alxhub added 2 commits October 3, 2018 15:30
Upcoming implementation work for template type-checking will need to reuse the
code which matches directives inside a template, so this refactor commit moves
the code to a shared location in preparation.

This commit pulls the code needed to match directives against a template node
out of the TemplateDefinitionBuilder into a utility function, in preparation
for template type-checking and other TemplateDefinitionBuilder refactoring.
This commit adds a generic type parameter to the SelectorMatcher
class and its associated response types. This makes the API for
matching selectors and obtaining information about the matched
directives significantly more ergonomic and type-safe.
@alxhub alxhub added the action: merge The PR is ready for merge by the caretaker label Oct 4, 2018
@jasonaden jasonaden closed this in 9cb17ec Oct 4, 2018
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
This commit adds a generic type parameter to the SelectorMatcher
class and its associated response types. This makes the API for
matching selectors and obtaining information about the matched
directives significantly more ergonomic and type-safe.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…26203)

This commit introduces the "t2" API, which processes parsed template ASTs
and performs a number of functions such as binding (the process of
semantically interpreting cross-references within the template) and
directive matching. The API is modeled on TypeScript's TypeChecker API,
with oracle methods that give access to collected metadata.

This work is a prerequisite for the upcoming template type-checking
functionality, and will also become the basis for a refactored
TemplateDefinitionBuilder.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…or (#26203)

This commit introduces //packages/compiler-cli/src/ngtsc/typecheck as a
container for template type-checking code, and implements an initial API:
type constructor generation.

Type constructors are static methods on component/directive types with
no runtime implementation. The methods are used during compilation to
enable inference of a component or directive's generic type parameters
from the types of expressions bound to any of their @inputs. A type
constructor looks like:

class Directive<T> {
  someInput: T;
  static ngTypeCtor<T>(init: Partial<Pick<Directive<T>, 'someInput'>>): Directive<T>;
}

It can be used to infer a type for T based on the input:

const _dir = Directive.ngTypeCtor({someInput: 'string'}); // Directive<T>

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…26203)

Previously in Ivy, metadata for directives/components/modules/etc was
carried in .d.ts files inside type information encoded on the
DirectiveDef, ComponentDef, NgModuleDef, etc types of Ivy definition
fields. This works well, but has the side effect of complicating Ivy's
runtime code as these extra generic type parameters had to be specified
as <any> throughout the codebase. *DefInternal types were introduced
previously to mitigate this issue, but that's the wrong way to solve
the problem.

This commit returns *Def types to their original form, with no metadata
attached. Instead, new *DefWithMeta types are introduced that alias the
plain definition types and add extra generic parameters. This way the
only code that needs to deal with the extra metadata parameters is the
compiler code that reads and writes them - the existence of this metadata
is transparent to the runtime, as it should be.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…6203)

Template type-checking will make use of expression and statement
translation as well as the ImportManager, so this code needs to
live in a separate build target which can be depended on by both
the main ngtsc transform as well as the template type-checking
mechanism. This refactor introduces a separate build target
for that code.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
This commit introduces the main functionality of the type-check compiler:
generation of type check blocks. Type check blocks are blocks of TypeScript
code which can be inlined into source files, and when processed by the
TypeChecker will give information about any typing errors in template
expressions.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
This commit introduces the template type-checking context API, which manages
inlining of type constructors and type-check blocks into ts.SourceFiles.
This API will be used by ngtsc to generate a type-checking ts.Program.

An TypeCheckProgramHost is provided which can wrap a normal ts.CompilerHost
and intercept getSourceFile() calls. This can be used to provide source
files with type check blocks to a ts.Program for type-checking.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…6203)

Before type checking can be turned on in ngtsc, appropriate metadata for
each component and directive must be determined. This commit adds tracking
of the extra metadata in *DefWithMeta types to the selector scope handling,
allowing for later extraction for type-checking purposes.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…#26203)

This commit adds an ngTemplateGuard_ngIf static method to the NgIf
directive and an ngTemplateContextGuard static method to NgFor. The
function of these two static methods is to enable type narrowing
within generated type checking code for consumers of the directives.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…26203)

This commit enables generation and checking of a type checking ts.Program
whenever the fullTemplateTypeCheck flag is enabled in tsconfig.json. It
puts together all the pieces built previously and causes diagnostics to be
emitted whenever type errors are discovered in a template.

Todos:

* map errors back to template HTML
* expand set of type errors covered in generated type-check blocks

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
…#26203)

This commit gets ready for the introduction of ngtsc template
type-checking tests by refactoring test environment setup into a
custom helper. This helper will simplify the authoring of future
ngtsc tests.

Ngtsc tests previously returned a numeric error code (a la ngtsc's CLI
interface) if any TypeScript errors occurred. The helper has the
ability to run ngtsc and return the actual array of ts.Diagnostics, which
greatly increases the ability to write clean tests.

PR Close #26203
jasonaden pushed a commit that referenced this pull request Oct 4, 2018
This commit builds on the NgtscTestEnvironment helper work before and
introduces template_typecheck_spec.ts, which contains compiler tests
for template type-checking.

PR Close #26203
@angular-automatic-lock-bot

Copy link
Copy Markdown

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot Bot locked and limited conversation to collaborators Sep 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

action: merge The PR is ready for merge by the caretaker cla: yes target: major This PR is targeted for the next major release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants