diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 1588c3b6d729a..70c6e8f0789a8 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -98,7 +98,19 @@ export function generateTypeCheckBlock( * Each `TcbOp` may insert statements into the body of the TCB, and also optionally return a * `ts.Expression` which can be used to reference the operation's result. */ -abstract class TcbOp { abstract execute(): ts.Expression|null; } +abstract class TcbOp { + abstract execute(): ts.Expression|null; + + /** + * Replacement value or operation used while this `TcbOp` is executing (i.e. to resolve circular + * references during its execution). + * + * This is usually a `null!` expression (which asks TS to infer an appropriate type), but another + * `TcbOp` can be returned in cases where additional code generation is necessary to deal with + * circular references. + */ + circularFallback(): TcbOp|ts.Expression { return INFER_TYPE_FOR_CIRCULAR_OP_EXPR; } +} /** * A `TcbOp` which creates an expression for a native DOM element (or web component) from a @@ -317,6 +329,41 @@ class TcbDirectiveOp extends TcbOp { this.scope.addStatement(tsCreateVariable(id, typeCtor)); return id; } + + circularFallback(): TcbOp { + return new TcbDirectiveCircularFallbackOp(this.tcb, this.scope, this.node, this.dir); + } +} + +/** + * A `TcbOp` which is used to generate a fallback expression if the inference of a directive type + * via `TcbDirectiveOp` requires a reference to its own type. This can happen using a template + * reference: + * + * ```html + * + * ``` + * + * In this case, `TcbDirectiveCircularFallbackOp` will add a second inference of the directive type + * to the type-check block, this time calling the directive's type constructor without any input + * expressions. This infers the widest possible supertype for the directive, which is used to + * resolve any recursive references required to infer the real type. + */ +class TcbDirectiveCircularFallbackOp extends TcbOp { + constructor( + private tcb: Context, private scope: Scope, private node: TmplAstTemplate|TmplAstElement, + private dir: TypeCheckableDirectiveMeta) { + super(); + } + + execute(): ts.Identifier { + const id = this.tcb.allocateId(); + const typeCtor = this.tcb.env.typeCtorFor(this.dir); + const circularPlaceholder = ts.createCall( + typeCtor, /* typeArguments */ undefined, [ts.createNonNullExpression(ts.createNull())]); + this.scope.addStatement(tsCreateVariable(id, circularPlaceholder)); + return id; + } } /** @@ -832,10 +879,10 @@ class Scope { return op; } - // Set the result of the operation in the queue to a special expression. If executing this - // operation results in a circular dependency, this will break the cycle and infer the least - // narrow type where needed (which is how TypeScript deals with circular dependencies in types). - this.opQueue[opIndex] = INFER_TYPE_FOR_CIRCULAR_OP_EXPR; + // Set the result of the operation in the queue to its circular fallback. If executing this + // operation results in a circular dependency, this will prevent an infinite loop and allow for + // the resolution of such cycles. + this.opQueue[opIndex] = op.circularFallback(); const res = op.execute(); // Once the operation has finished executing, it's safe to cache the real result. this.opQueue[opIndex] = res; diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index e1955bd55fafe..05d9d4cbe4b9d 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -180,7 +180,10 @@ describe('type check blocks', () => { exportAs: ['dir'], inputs: {input: 'input'}, }]; - expect(tcb(TEMPLATE, DIRECTIVES)).toContain('var _t2 = Dir.ngTypeCtor({ "input": (null!) });'); + expect(tcb(TEMPLATE, DIRECTIVES)) + .toContain( + 'var _t3 = Dir.ngTypeCtor((null!)); ' + + 'var _t2 = Dir.ngTypeCtor({ "input": (_t3) });'); }); it('should generate circular references between two directives correctly', () => { @@ -206,7 +209,8 @@ describe('type check blocks', () => { ]; expect(tcb(TEMPLATE, DIRECTIVES)) .toContain( - 'var _t3 = DirB.ngTypeCtor({ "inputA": (null!) }); ' + + 'var _t4 = DirA.ngTypeCtor((null!)); ' + + 'var _t3 = DirB.ngTypeCtor({ "inputA": (_t4) }); ' + 'var _t2 = DirA.ngTypeCtor({ "inputA": (_t3) });'); }); diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index dd0c4d5c74fff..e299e674528aa 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -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: '', + }) + 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', `