Skip to content

Commit 645447d

Browse files
crisbetothePunderWoman
authored andcommitted
fix(compiler-cli): incorrect inferred type of for loop implicit variables (#52732)
Fixes that all implicit variables in `@for` loops were inferred to be numbers, even though most are actually boolean. Note that I also had to work around a weird TypeScript behavior in `tsDeclareVariable` where usually we declare variables in the following format: ``` var _t1: <type> = null!; ``` This works in most cases, but if the type is a `boolean`, TypeScript infers the variable as `never`, instead of `boolean`. I've worked around it by adding an `as boolean` to the initializer. Fixes #52730. PR Close #52732
1 parent 8a87e62 commit 645447d

File tree

4 files changed

+45
-20
lines changed

4 files changed

+45
-20
lines changed

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,21 @@ export function tsCreateElement(tagName: string): ts.Expression {
7777
* Unlike with `tsCreateVariable`, the type of the variable is explicitly specified.
7878
*/
7979
export function tsDeclareVariable(id: ts.Identifier, type: ts.TypeNode): ts.VariableStatement {
80+
let initializer: ts.Expression = ts.factory.createNonNullExpression(ts.factory.createNull());
81+
82+
// When we create a variable like `var _t1: boolean = null!`, TypeScript actually infers `_t1`
83+
// to be `never`, instead of a `boolean`. To work around it, we cast the value to boolean again
84+
// in the initializer, e.g. `var _t1: boolean = null! as boolean;`.
85+
if (type.kind === ts.SyntaxKind.BooleanKeyword) {
86+
initializer = ts.factory.createAsExpression(
87+
initializer, ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword));
88+
}
89+
8090
const decl = ts.factory.createVariableDeclaration(
8191
/* name */ id,
8292
/* exclamationToken */ undefined,
8393
/* type */ type,
84-
/* initializer */ ts.factory.createNonNullExpression(ts.factory.createNull()));
94+
/* initializer */ initializer);
8595
return ts.factory.createVariableStatement(
8696
/* modifiers */ undefined,
8797
/* declarationList */[decl]);

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,18 @@ class Scope {
15721572
*/
15731573
private statements: ts.Statement[] = [];
15741574

1575+
/**
1576+
* Names of the for loop context variables and their types.
1577+
*/
1578+
private static readonly forLoopContextVariableTypes = new Map<string, ts.KeywordTypeSyntaxKind>([
1579+
['$first', ts.SyntaxKind.BooleanKeyword],
1580+
['$last', ts.SyntaxKind.BooleanKeyword],
1581+
['$even', ts.SyntaxKind.BooleanKeyword],
1582+
['$odd', ts.SyntaxKind.BooleanKeyword],
1583+
['$index', ts.SyntaxKind.NumberKeyword],
1584+
['$count', ts.SyntaxKind.NumberKeyword],
1585+
]);
1586+
15751587
private constructor(
15761588
private tcb: Context, private parent: Scope|null = null,
15771589
private guard: ts.Expression|null = null) {}
@@ -1628,9 +1640,12 @@ class Scope {
16281640
new TcbBlockVariableOp(
16291641
tcb, scope, ts.factory.createIdentifier(scopedNode.item.name), scopedNode.item));
16301642

1631-
for (const variable of Object.values(scopedNode.contextVariables)) {
1632-
// Note: currently all context variables are assumed to be number types.
1633-
const type = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
1643+
for (const [name, variable] of Object.entries(scopedNode.contextVariables)) {
1644+
if (!this.forLoopContextVariableTypes.has(name)) {
1645+
throw new Error(`Unrecognized for loop context variable ${name}`);
1646+
}
1647+
1648+
const type = ts.factory.createKeywordTypeNode(this.forLoopContextVariableTypes.get(name)!);
16341649
this.registerVariable(
16351650
scope, variable, new TcbBlockImplicitVariableOp(tcb, scope, type, variable));
16361651
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1560,10 +1560,10 @@ describe('type check blocks', () => {
15601560
const result = tcb(TEMPLATE);
15611561
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
15621562
expect(result).toContain('var _t2: number = null!;');
1563-
expect(result).toContain('var _t3: number = null!;');
1564-
expect(result).toContain('var _t4: number = null!;');
1565-
expect(result).toContain('var _t5: number = null!;');
1566-
expect(result).toContain('var _t6: number = null!;');
1563+
expect(result).toContain('var _t3: boolean = null! as boolean;');
1564+
expect(result).toContain('var _t4: boolean = null! as boolean;');
1565+
expect(result).toContain('var _t5: boolean = null! as boolean;');
1566+
expect(result).toContain('var _t6: boolean = null! as boolean;');
15671567
expect(result).toContain('var _t7: number = null!;');
15681568
expect(result).toContain('"" + (_t2) + (_t3) + (_t4) + (_t5) + (_t6) + (_t7)');
15691569
});
@@ -1578,10 +1578,10 @@ describe('type check blocks', () => {
15781578
const result = tcb(TEMPLATE);
15791579
expect(result).toContain('for (const item of ((this).items)!) { var _t1 = item;');
15801580
expect(result).toContain('var _t2: number = null!;');
1581-
expect(result).toContain('var _t3: number = null!;');
1582-
expect(result).toContain('var _t4: number = null!;');
1583-
expect(result).toContain('var _t5: number = null!;');
1584-
expect(result).toContain('var _t6: number = null!;');
1581+
expect(result).toContain('var _t3: boolean = null! as boolean;');
1582+
expect(result).toContain('var _t4: boolean = null! as boolean;');
1583+
expect(result).toContain('var _t5: boolean = null! as boolean;');
1584+
expect(result).toContain('var _t6: boolean = null! as boolean;');
15851585
expect(result).toContain('var _t7: number = null!;');
15861586
expect(result).toContain('"" + (_t2) + (_t3) + (_t4) + (_t5) + (_t6) + (_t7)');
15871587
});

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4477,10 +4477,10 @@ suppress
44774477
const diags = env.driveDiagnostics();
44784478
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
44794479
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4480-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4481-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4482-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4483-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4480+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
4481+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
4482+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
4483+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
44844484
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
44854485
]);
44864486
});
@@ -4513,10 +4513,10 @@ suppress
45134513
const diags = env.driveDiagnostics();
45144514
expect(diags.map(d => ts.flattenDiagnosticMessageText(d.messageText, ''))).toEqual([
45154515
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4516-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4517-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4518-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4519-
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
4516+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
4517+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
4518+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
4519+
`Argument of type 'boolean' is not assignable to parameter of type 'string'.`,
45204520
`Argument of type 'number' is not assignable to parameter of type 'string'.`,
45214521
]);
45224522
});

0 commit comments

Comments
 (0)