From 0937062a642b500d9862cd217bfe48899ee2bf99 Mon Sep 17 00:00:00 2001 From: JoostK Date: Sun, 28 Apr 2019 16:20:16 +0200 Subject: [PATCH] fix(ivy): type-checking should infer string type for interpolations (#30177) Previously, interpolations were generated into TCBs as a comma-separated list of expressions, letting TypeScript infer the type of the expression as the type of the last expression in the chain. This is undesirable, as interpolations always result in a string type at runtime. Therefore, type-checking of bindings such as `` where `link` is an object would incorrectly report a type-error. This commit adjusts the emitted TCB code for interpolations, where a chain of string concatenations is emitted, starting with the empty string. This ensures that the inferred type of the interpolation is of type string. PR Close #30177 --- .../src/ngtsc/typecheck/src/expression.ts | 21 ++++++------------- .../typecheck/test/type_check_block_spec.ts | 8 +++---- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts index abe2e0ae50..9228824a4a 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/expression.ts @@ -93,7 +93,12 @@ class AstTranslator implements AstVisitor { } visitInterpolation(ast: Interpolation): ts.Expression { - return this.astArrayToExpression(ast.expressions); + // Build up a chain of binary + operations to simulate the string concatenation of the + // interpolation's expressions. The chain is started using an actual string literal to ensure + // the type is inferred as 'string'. + return ast.expressions.reduce( + (lhs, ast) => ts.createBinary(lhs, ts.SyntaxKind.PlusToken, this.translate(ast)), + ts.createLiteral('')); } visitKeyedRead(ast: KeyedRead): ts.Expression { @@ -177,20 +182,6 @@ class AstTranslator implements AstVisitor { const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; return safeTernary(receiver, expr, whenNull); } - - /** - * Convert an array of `AST` expressions into a single `ts.Expression`, by converting them all - * and separating them with commas. - */ - private astArrayToExpression(astArray: AST[]): ts.Expression { - // Reduce the `asts` array into a `ts.Expression`. Multiple expressions are combined into a - // `ts.BinaryExpression` with a comma separator. First make a copy of the input array, as - // it will be modified during the reduction. - const asts = astArray.slice(); - return asts.reduce( - (lhs, ast) => ts.createBinary(lhs, ts.SyntaxKind.CommaToken, this.translate(ast)), - this.translate(asts.pop() !)); - } } function safeTernary( 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 aed6cd41b4..84887b8c23 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 @@ -18,7 +18,7 @@ import {generateTypeCheckBlock} from '../src/type_check_block'; describe('type check blocks', () => { it('should generate a basic block for a binding', - () => { expect(tcb('{{hello}}')).toContain('ctx.hello;'); }); + () => { expect(tcb('{{hello}} {{world}}')).toContain('"" + ctx.hello + ctx.world;'); }); it('should generate literal map expressions', () => { const TEMPLATE = '{{ method({foo: a, bar: b}) }}'; @@ -32,7 +32,7 @@ describe('type check blocks', () => { it('should handle non-null assertions', () => { const TEMPLATE = `{{a!}}`; - expect(tcb(TEMPLATE)).toContain('ctx.a!;'); + expect(tcb(TEMPLATE)).toContain('(ctx.a!);'); }); it('should handle keyed property access', () => { @@ -45,7 +45,7 @@ describe('type check blocks', () => { {{ i.value }} `; - expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); _t1.value;'); + expect(tcb(TEMPLATE)).toContain('var _t1 = document.createElement("input"); "" + _t1.value;'); }); it('should generate a forward directive reference correctly', () => { @@ -61,7 +61,7 @@ describe('type check blocks', () => { }]; expect(tcb(TEMPLATE, DIRECTIVES)) .toContain( - 'var _t1 = Dir.ngTypeCtor({}); _t1.value; var _t2 = document.createElement("div");'); + 'var _t1 = Dir.ngTypeCtor({}); "" + _t1.value; var _t2 = document.createElement("div");'); }); it('should handle style and class bindings specially', () => {