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 d7b03f5523)
This commit is contained in:
Matthieu Riegler
2026-08-05 16:57:14 +02:00
committed by Alon Mishne
parent 4f7e9987fa
commit 76dff307b4
3 changed files with 29 additions and 4 deletions
@@ -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)', () => {
@@ -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)', () => {
+19 -3
View File
@@ -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 {