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 {
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(

View File

@ -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 }}
<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', () => {
@ -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', () => {