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 ca180c06ace..e8761743d4b 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 @@ -1508,6 +1508,12 @@ describe('type check blocks', () => { expect(block).toContain('(((((this).a))![0] as any)'); expect(block).toContain('((((((this).a)).optionalMethod))!() as any)'); }); + + it('should produce correct correct ts expression', () => { + const TEMPLATE = `{{ one?.two.three }}`; + const block = tcb(TEMPLATE, DIRECTIVES); + expect(block).toContain('(((((this).one))?.two.three))'); + }); }); describe('config.strictSafeNavigationTypes (View Engine bug emulation)', () => { diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts index c4032409627..fa008e36e5b 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_checker__get_symbol_of_template_node_spec.ts @@ -836,11 +836,14 @@ runInEachFileSystem(() => { .declarations![0] as ts.PropertyDeclaration ).parent.name!.getText(), ).toEqual('Car'); + + // Even if engine is string, TS will returned the type returned by the expression, + // which is string | undefined because of the safe navigation operator. expect( program .getTypeChecker() .typeToString(templateTypeChecker.getTypeOfSymbol(keyedReadSymbol)!), - ).toEqual('string'); + ).toEqual('string | undefined'); }); it('safe property reads with as any (failure case)', () => { diff --git a/packages/compiler/src/typecheck/expression.ts b/packages/compiler/src/typecheck/expression.ts index ef40af0747a..618ba43ec2e 100644 --- a/packages/compiler/src/typecheck/expression.ts +++ b/packages/compiler/src/typecheck/expression.ts @@ -143,7 +143,10 @@ class TcbExprTranslator implements AstVisitor { } visitKeyedRead(ast: KeyedRead): TcbExpr { - const receiver = this.translate(ast.receiver).wrapForTypeChecker(); + const receiver = this.translate(ast.receiver); + if (!this.isStrictSafeNavigationChain(ast.receiver)) { + receiver.wrapForTypeChecker(); + } const key = this.translate(ast.key); return new TcbExpr(`${receiver.print()}[${key.print()}]`).addParseSpanInfo(ast.sourceSpan); } @@ -234,7 +237,10 @@ class TcbExprTranslator implements AstVisitor { } visitPropertyRead(ast: PropertyRead): TcbExpr { - const receiver = this.translate(ast.receiver).wrapForTypeChecker(); + const receiver = this.translate(ast.receiver); + if (!this.isStrictSafeNavigationChain(ast.receiver)) { + receiver.wrapForTypeChecker(); + } return new TcbExpr(`${receiver.print()}.${ast.name}`) .addParseSpanInfo(ast.nameSpan) .wrapForTypeChecker() @@ -284,7 +290,10 @@ class TcbExprTranslator implements AstVisitor { if (resolved !== null) { expr = resolved; } else { - const propertyReceiver = this.translate(receiver.receiver).wrapForTypeChecker(); + const propertyReceiver = this.translate(receiver.receiver); + if (!this.isStrictSafeNavigationChain(receiver.receiver)) { + propertyReceiver.wrapForTypeChecker(); + } expr = new TcbExpr(`${propertyReceiver.print()}.${receiver.name}`).addParseSpanInfo( receiver.nameSpan, ); @@ -414,6 +423,13 @@ class TcbExprTranslator implements AstVisitor { private escapeTemplateLiteral(value: string) { return value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\${/g, '$\\{'); } + + private isStrictSafeNavigationChain(ast: AST): boolean { + return ( + this.config.strictSafeNavigationTypes && + (ast instanceof SafePropertyRead || ast instanceof SafeKeyedRead || ast instanceof SafeCall) + ); + } } class VeSafeLhsInferenceBugDetector implements AstVisitor {