Repository navigation
Conversation
|
You can preview d1a608d at https://pr26203-d1a608d.ngbuilds.io/. |
There was a problem hiding this comment.
There does not seem to be an assertion. If you checking that it does not throw, say so.
There was a problem hiding this comment.
driveMain asserts that the compiler ran successfully. This pattern is used in all the tests.
There was a problem hiding this comment.
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>
}There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>(...)
}There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
NgIfand 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_ContextThis 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)
d1a608d to
e25a598
Compare
|
You can preview e25a598 at https://pr26203-e25a598.ngbuilds.io/. |
JoostK
left a comment
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Why is the param optional, now we have to cheat here?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
If no directive guards, why generate if (true) { ... }, where just { ... } would suffice to create a new block?
There was a problem hiding this comment.
No particular reason. This way all the blocks get generated with the same structure.
There was a problem hiding this comment.
Did TS not infer non-nullable node.name here? It is guarded at the top.
There was a problem hiding this comment.
It is currently not tested if the cached SF was already transformed, is it?
There was a problem hiding this comment.
Why not JSON.stringify this whole object literal?
There was a problem hiding this comment.
No reason - this was moved code, I didn't modify the semantics.
e25a598 to
fab939e
Compare
|
You can preview fab939e at https://pr26203-fab939e.ngbuilds.io/. |
fab939e to
ca2d072
Compare
|
You can preview ca2d072 at https://pr26203-ca2d072.ngbuilds.io/. |
ca2d072 to
702bebe
Compare
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
D'oh, of course it is. That's what I get for going too fast.
Fixed for real this time.
|
You can preview 702bebe at https://pr26203-702bebe.ngbuilds.io/. |
702bebe to
57c5c2d
Compare
|
You can preview 57c5c2d at https://pr26203-57c5c2d.ngbuilds.io/. |
ea8c942 to
3b36369
Compare
|
You can preview 3b36369 at https://pr26203-3b36369.ngbuilds.io/. |
3b36369 to
15f851c
Compare
|
You can preview 15f851c at https://pr26203-15f851c.ngbuilds.io/. |
15f851c to
e28e2e4
Compare
|
You can preview e28e2e4 at https://pr26203-e28e2e4.ngbuilds.io/. |
e28e2e4 to
96b406b
Compare
|
You can preview 96b406b at https://pr26203-96b406b.ngbuilds.io/. |
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.
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
…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
…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
…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
…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
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
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
…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
…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
…#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
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
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
This extra-large PR implements major components of template type-checking in ngtsc. Broadly, it includes:
ts.Programcreated for the purposes of type checking.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.