From 76dff307b4c34bc4ceefcb1c7aa86c39daa5ea45 Mon Sep 17 00:00:00 2001 From: Matthieu Riegler Date: Wed, 5 Aug 2026 16:57:14 +0200 Subject: [PATCH] fix(compiler): Generate correct expression for optional chaning. Optional chaining was generating expressions with included an extra pair of parenthesis which changed the semantics of the expression and threw an unexpected error from the optional chain non nullable extended diagnostic. fixes #70085 (cherry picked from commit d7b03f5523d220c7935426d23ff5f103c96b00d9) --- .../typecheck/test/type_check_block_spec.ts | 6 +++++ ...ecker__get_symbol_of_template_node_spec.ts | 5 ++++- packages/compiler/src/typecheck/expression.ts | 22 ++++++++++++++++--- 3 files changed, 29 insertions(+), 4 deletions(-) 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 {