fix(ivy): better inference for circularly referenced directive types (#35622)

It's possible to pass a directive as an input to itself. Consider:

```html
<some-cmp #ref [value]="ref">
```

Since the template type-checker attempts to infer a type for `<some-cmp>`
using the values of its inputs, this creates a circular reference where the
type of the `value` input is used in its own inference:

```typescript
var _t0 = SomeCmp.ngTypeCtor({value: _t0});
```

Obviously, this doesn't work. To resolve this, the template type-checker
used to generate a `null!` expression when a reference would otherwise be
circular:

```typescript
var _t0 = SomeCmp.ngTypeCtor({value: null!});
```

This effectively asks TypeScript to infer a value for this context, and
works well to resolve this simple cycle. However, if the template
instead tries to use the circular value in a larger expression:

```html
<some-cmp #ref [value]="ref.prop">
```

The checker would generate:

```typescript
var _t0 = SomeCmp.ngTypeCtor({value: (null!).prop});
```

In this case, TypeScript can't figure out any way `null!` could have a
`prop` key, and so it infers `never` as the type. `(never).prop` is thus a
type error.

This commit implements a better fallback pattern for circular references to
directive types like this. Instead of generating a `null!` in place for the
reference, a type is inferred by calling the type constructor again with
`null!` as its input. This infers the widest possible type for the directive
which is then used to break the cycle:

```typescript
var _t0 = SomeCmp.ngTypeCtor(null!);
var _t1 = SomeCmp.ngTypeCtor({value: _t0.prop});
```

This has the desired effect of validating that `.prop` is legal for the
directive type (the type of `#ref`) while also avoiding a cycle.

Fixes #35372
Fixes #35603
Fixes #35522

PR Close #35622
This commit is contained in:
Alex Rickabaugh
2020-02-21 16:06:17 -08:00
committed by Miško Hevery
parent 2d89b5d13d
commit 173a1ac8e4
3 changed files with 84 additions and 7 deletions

View File

@ -293,6 +293,32 @@ export declare class AnimationEvent {
expect(diags.length).toBe(0);
});
it('should support a directive being used in its own input expression', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
import {Component, Directive, NgModule, Input} from '@angular/core';
@Component({
selector: 'test',
template: '<target-cmp #ref [foo]="ref.bar"></target-cmp>',
})
export class TestCmp {}
@Component({template: '', selector: 'target-cmp'})
export class TargetCmp {
readonly bar = 'test';
@Input() foo: string;
}
@NgModule({
declarations: [TestCmp, TargetCmp],
})
export class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(0);
});
describe('strictInputTypes', () => {
beforeEach(() => {
env.write('test.ts', `