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:

committed by
Miško Hevery

parent
2d89b5d13d
commit
173a1ac8e4
@ -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', `
|
||||
|
Reference in New Issue
Block a user