Skip to content

Commit 0eae992

Browse files
crisbetoalxhub
authored andcommitted
fix(compiler): allow nullable values in for loop block (#51997)
Updates the TCB for `@for` loop blocks to allow nullable values. The runtime already supports it and this makes it easier to switch from `NgFor`. Fixes #51993. PR Close #51997
1 parent 7d42dc3 commit 0eae992

File tree

3 files changed

+62
-8
lines changed

3 files changed

+62
-8
lines changed

packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1297,7 +1297,10 @@ class TcbForOfOp extends TcbOp {
12971297
const loopScope = Scope.forNodes(this.tcb, this.scope, this.block, null);
12981298
const initializer = ts.factory.createVariableDeclarationList(
12991299
[ts.factory.createVariableDeclaration(this.block.item.name)], ts.NodeFlags.Const);
1300-
const expression = tcbExpression(this.block.expression, this.tcb, loopScope);
1300+
// It's common to have a for loop over a nullable value (e.g. produced by the `async` pipe).
1301+
// Add a non-null expression to allow such values to be assigned.
1302+
const expression = ts.factory.createNonNullExpression(
1303+
tcbExpression(this.block.expression, this.tcb, loopScope));
13011304
const trackTranslator = new TcbForLoopTrackTranslator(this.tcb, loopScope, this.block);
13021305
const trackExpression = trackTranslator.translate(this.block.trackBy);
13031306
const statements = [

packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,7 +1485,7 @@ describe('type check blocks', () => {
14851485
`;
14861486

14871487
const result = tcb(TEMPLATE);
1488-
expect(result).toContain('for (const item of ((this).items)) { var _t1 = item;');
1488+
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
14891489
expect(result).toContain('"" + ((this).main(_t1))');
14901490
expect(result).toContain('"" + ((this).empty())');
14911491
});
@@ -1498,7 +1498,7 @@ describe('type check blocks', () => {
14981498
`;
14991499

15001500
const result = tcb(TEMPLATE);
1501-
expect(result).toContain('for (const item of ((this).items)) { var _t1 = item;');
1501+
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
15021502
expect(result).toContain('var _t2: number = null!;');
15031503
expect(result).toContain('var _t3: number = null!;');
15041504
expect(result).toContain('var _t4: number = null!;');
@@ -1516,7 +1516,7 @@ describe('type check blocks', () => {
15161516
`;
15171517

15181518
const result = tcb(TEMPLATE);
1519-
expect(result).toContain('for (const item of ((this).items)) { var _t1 = item;');
1519+
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
15201520
expect(result).toContain('var _t2: number = null!;');
15211521
expect(result).toContain('var _t3: number = null!;');
15221522
expect(result).toContain('var _t4: number = null!;');
@@ -1532,7 +1532,7 @@ describe('type check blocks', () => {
15321532
`;
15331533

15341534
const result = tcb(TEMPLATE);
1535-
expect(result).toContain('for (const item of ((this).items)) { var _t1 = item;');
1535+
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
15361536
expect(result).toContain('var _t2: number = null!;');
15371537
expect(result).toContain('"" + (((this).$index)) + (_t2)');
15381538
});
@@ -1550,18 +1550,18 @@ describe('type check blocks', () => {
15501550

15511551
const result = tcb(TEMPLATE);
15521552
expect(result).toContain(
1553-
'for (const item of ((this).items)) { var _t1 = item; var _t2: number = null!;');
1553+
'for (const item of ((this).items)!) { var _t1 = item; var _t2: number = null!;');
15541554
expect(result).toContain('"" + (_t1) + (_t2)');
15551555
expect(result).toContain(
1556-
'for (const inner of ((_t1).items)) { var _t8 = inner; var _t9: number = null!;');
1556+
'for (const inner of ((_t1).items)!) { var _t8 = inner; var _t9: number = null!;');
15571557
expect(result).toContain('"" + (_t1) + (_t2) + (_t8) + (_t9)');
15581558
});
15591559

15601560
it('should generate the tracking expression of a for loop', () => {
15611561
const result = tcb(`@for (item of items; track trackingFn($index, item, prop)) {}`);
15621562

15631563
expect(result).toContain(
1564-
'for (const item of ((this).items)) { var _t1: number = null!; var _t2 = item;');
1564+
'for (const item of ((this).items)!) { var _t1: number = null!; var _t2 = item;');
15651565
expect(result).toContain('(this).trackingFn(_t1, _t2, ((this).prop));');
15661566
});
15671567
});

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4495,6 +4495,57 @@ suppress
44954495
expect(diags[0].messageText)
44964496
.toContain('Error: Illegal State: Pipes are not allowed in this context');
44974497
});
4498+
4499+
it('should allow nullable values in loop expression', () => {
4500+
env.write('test.ts', `
4501+
import {Component, Pipe} from '@angular/core';
4502+
4503+
@Pipe({name: 'fakeAsync', standalone: true})
4504+
export class FakeAsyncPipe {
4505+
transform<T>(value: Iterable<T>): Iterable<T> | null | undefined {
4506+
return null;
4507+
}
4508+
}
4509+
4510+
@Component({
4511+
template: \`
4512+
@for (item of items | fakeAsync; track item) {
4513+
{{item}}
4514+
}
4515+
\`,
4516+
standalone: true,
4517+
imports: [FakeAsyncPipe]
4518+
})
4519+
export class Main {
4520+
items = [];
4521+
}
4522+
`);
4523+
4524+
const diags = env.driveDiagnostics();
4525+
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([]);
4526+
});
4527+
4528+
it('should enforce that the loop expression is iterable', () => {
4529+
env.write('test.ts', `
4530+
import {Component} from '@angular/core';
4531+
4532+
@Component({
4533+
template: \`
4534+
@for (item of items; track item) {
4535+
{{item}}
4536+
}
4537+
\`,
4538+
})
4539+
export class Main {
4540+
items = 123;
4541+
}
4542+
`);
4543+
4544+
const diags = env.driveDiagnostics();
4545+
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
4546+
`Type 'number' must have a '[Symbol.iterator]()' method that returns an iterator.`
4547+
]);
4548+
});
44984549
});
44994550
});
45004551
});

0 commit comments

Comments
 (0)