feat(core): Allow other expression for exhaustive typechecking

When the switched expression is nested within a union, exhaustive typechecking needs to know which expression to check.
This change adds the possibility of specifying the expression to check:

```
@Component({
  selector: 'app-root',
  imports: [],
  template: `
    @switch (state.mode) {
      @case ('show') { {{ state.menu }}; }
      @case ('hide') {}
      @default never(state);
    }
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
  state!: { mode: 'hide' } | { mode: 'show'; menu: number };;
}
```

fixes #67406
This commit is contained in:
Matthieu Riegler
2026-03-19 01:49:21 +01:00
committed by Leon Senft
parent 0eeb1b5f03
commit 8bc31a515f
13 changed files with 147 additions and 22 deletions
@@ -60,7 +60,12 @@ export class TcbSwitchOp extends TcbOp {
});
if (this.block.exhaustiveCheck) {
const switchValue = tcbExpression(this.block.expression, this.tcb, this.scope);
let translateExpression = this.block.expression;
if (this.block.exhaustiveCheck.expression) {
translateExpression = this.block.exhaustiveCheck.expression;
}
const switchValue = tcbExpression(translateExpression, this.tcb, this.scope);
const exhaustiveId = this.tcb.allocateId();
clauses.push(
@@ -1453,6 +1453,25 @@ class TestComponent {
expect(messages).toEqual([]);
});
it('should narrow union when switching on a nested prop', () => {
const messages = diagnose(
`
@switch (state.mode) {
@case ('show') { {{ state.menu }}; }
@case ('hide') {}
@default never(state);
}
`,
`
export class TestComponent {
state: { mode: 'hide' } | { mode: 'show'; menu: number };
}
`,
);
expect(messages).toEqual([]);
});
});
// https://github.com/angular/angular/issues/43970
@@ -2202,6 +2202,27 @@ describe('type check blocks', () => {
);
});
it('should generate a switch block with exhaustiveness checking with param', () => {
const TEMPLATE = `
@switch (expr) {
@case (1) {
{{one()}}
}
@case (2) {
{{two()}}
}
@default never(expr.prop);
}
`;
expect(tcb(TEMPLATE)).toContain(
'switch (((this).expr)) { ' +
'case 1: "" + ((this).one()); break; ' +
'case 2: "" + ((this).two()); break; ' +
'default: const tcbExhaustive_t1: never = ((((this).expr)).prop);',
);
});
it('should not report unused locals for exhaustiveness check variable', () => {
const TEMPLATE = `
@switch (expr) {