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 `<img src="{{ link }}"/>` 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
This commit is contained in:
JoostK 2019-04-28 16:20:16 +02:00 committed by Jason Aden
parent 6f073885b0
commit 0937062a64
2 changed files with 10 additions and 19 deletions

View File

@ -93,7 +93,12 @@ class AstTranslator implements AstVisitor {
} }
visitInterpolation(ast: Interpolation): ts.Expression { 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 { visitKeyedRead(ast: KeyedRead): ts.Expression {
@ -177,20 +182,6 @@ class AstTranslator implements AstVisitor {
const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY; const whenNull = this.config.strictSafeNavigationTypes ? UNDEFINED : NULL_AS_ANY;
return safeTernary(receiver, expr, whenNull); 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( function safeTernary(

View File

@ -18,7 +18,7 @@ import {generateTypeCheckBlock} from '../src/type_check_block';
describe('type check blocks', () => { describe('type check blocks', () => {
it('should generate a basic block for a binding', 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', () => { it('should generate literal map expressions', () => {
const TEMPLATE = '{{ method({foo: a, bar: b}) }}'; const TEMPLATE = '{{ method({foo: a, bar: b}) }}';
@ -32,7 +32,7 @@ describe('type check blocks', () => {
it('should handle non-null assertions', () => { it('should handle non-null assertions', () => {
const TEMPLATE = `{{a!}}`; const TEMPLATE = `{{a!}}`;
expect(tcb(TEMPLATE)).toContain('ctx.a!;'); expect(tcb(TEMPLATE)).toContain('(ctx.a!);');
}); });
it('should handle keyed property access', () => { it('should handle keyed property access', () => {
@ -45,7 +45,7 @@ describe('type check blocks', () => {
{{ i.value }} {{ i.value }}
<input #i> <input #i>
`; `;
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', () => { it('should generate a forward directive reference correctly', () => {
@ -61,7 +61,7 @@ describe('type check blocks', () => {
}]; }];
expect(tcb(TEMPLATE, DIRECTIVES)) expect(tcb(TEMPLATE, DIRECTIVES))
.toContain( .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', () => { it('should handle style and class bindings specially', () => {