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:
parent
6f073885b0
commit
0937062a64
@ -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(
|
||||||
|
@ -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', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user