diff --git a/packages/common/src/directives/ng_if.ts b/packages/common/src/directives/ng_if.ts index d143a3913e..07460feb3d 100644 --- a/packages/common/src/directives/ng_if.ts +++ b/packages/common/src/directives/ng_if.ts @@ -152,7 +152,8 @@ export class NgIf { } } - public static ngIfTypeGuard: (v: T|null|undefined|false) => v is T; + /** @internal */ + public static ngIfUseIfTypeGuard: void; } /** diff --git a/packages/compiler-cli/test/diagnostics/check_types_spec.ts b/packages/compiler-cli/test/diagnostics/check_types_spec.ts index bc1580515e..00d48489ad 100644 --- a/packages/compiler-cli/test/diagnostics/check_types_spec.ts +++ b/packages/compiler-cli/test/diagnostics/check_types_spec.ts @@ -216,6 +216,304 @@ describe('ng type checker', () => { export class MainModule {}` }); }); + + it('should narrow an *ngIf like directive with UseIf', () => { + a({ + 'src/app.component.ts': '', + 'src/lib.ts': '', + 'src/app.module.ts': ` + import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core'; + + export interface Person { + name: string; + } + + @Component({ + selector: 'comp', + template: '
{{person.name}}
' + }) + export class MainComp { + person?: Person; + } + + export class MyIfContext { + public $implicit: any = null; + public myIf: any = null; + } + + @Directive({selector: '[myIf]'}) + export class MyIf { + constructor(templateRef: TemplateRef) {} + + @Input() + set myIf(condition: any) {} + + static myIfUseIfTypeGuard: void; + } + + @NgModule({ + declarations: [MainComp, MyIf], + }) + export class MainModule {}` + }); + }); + + it('should narrow a renamed *ngIf like directive with UseIf', () => { + a({ + 'src/app.component.ts': '', + 'src/lib.ts': '', + 'src/app.module.ts': ` + import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core'; + + export interface Person { + name: string; + } + + @Component({ + selector: 'comp', + template: '
{{person.name}}
' + }) + export class MainComp { + person?: Person; + } + + export class MyIfContext { + public $implicit: any = null; + public myIf: any = null; + } + + @Directive({selector: '[my-if]'}) + export class MyIf { + constructor(templateRef: TemplateRef) {} + + @Input('my-if') + set myIf(condition: any) {} + + static myIfUseIfTypeGuard: void; + } + + @NgModule({ + declarations: [MainComp, MyIf], + }) + export class MainModule {}` + }); + }); + + it('should narrow a type in a nested *ngIf like directive with UseIf', () => { + a({ + 'src/app.component.ts': '', + 'src/lib.ts': '', + 'src/app.module.ts': ` + import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core'; + + export interface Address { + street: string; + } + + export interface Person { + name: string; + address?: Address; + } + + + @Component({ + selector: 'comp', + template: '
{{person.name}} {{person.address.street}}
' + }) + export class MainComp { + person?: Person; + } + + export class MyIfContext { + public $implicit: any = null; + public myIf: any = null; + } + + @Directive({selector: '[myIf]'}) + export class MyIf { + constructor(templateRef: TemplateRef) {} + + @Input() + set myIf(condition: any) {} + + static myIfUseIfTypeGuard: void; + } + + @NgModule({ + declarations: [MainComp, MyIf], + }) + export class MainModule {}` + }); + }); + + it('should narrow an *ngIf like directive with UseIf and &&', () => { + a({ + 'src/app.component.ts': '', + 'src/lib.ts': '', + 'src/app.module.ts': ` + import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core'; + + export interface Address { + street: string; + } + + export interface Person { + name: string; + } + + @Component({ + selector: 'comp', + template: '
{{person.name}} lives at {{address.street}}
' + }) + export class MainComp { + person?: Person; + address?: Address; + } + + export class MyIfContext { + public $implicit: any = null; + public myIf: any = null; + } + + @Directive({selector: '[myIf]'}) + export class MyIf { + constructor(templateRef: TemplateRef) {} + + @Input() + set myIf(condition: any) {} + + static myIfUseIfTypeGuard: void; + } + + @NgModule({ + declarations: [MainComp, MyIf], + }) + export class MainModule {}` + }); + }); + + it('should narrow an *ngIf like directive with UseIf and !!', () => { + a({ + 'src/app.component.ts': '', + 'src/lib.ts': '', + 'src/app.module.ts': ` + import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core'; + + export interface Person { + name: string; + } + + @Component({ + selector: 'comp', + template: '
{{person.name}}
' + }) + export class MainComp { + person?: Person; + } + + export class MyIfContext { + public $implicit: any = null; + public myIf: any = null; + } + + @Directive({selector: '[myIf]'}) + export class MyIf { + constructor(templateRef: TemplateRef) {} + + @Input() + set myIf(condition: any) {} + + static myIfUseIfTypeGuard: void; + } + + @NgModule({ + declarations: [MainComp, MyIf], + }) + export class MainModule {}` + }); + }); + + it('should narrow an *ngIf like directive with UseIf and != null', () => { + a({ + 'src/app.component.ts': '', + 'src/lib.ts': '', + 'src/app.module.ts': ` + import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core'; + + export interface Person { + name: string; + } + + @Component({ + selector: 'comp', + template: '
{{person.name}}
' + }) + export class MainComp { + person: Person | null = null; + } + + export class MyIfContext { + public $implicit: any = null; + public myIf: any = null; + } + + @Directive({selector: '[myIf]'}) + export class MyIf { + constructor(templateRef: TemplateRef) {} + + @Input() + set myIf(condition: any) {} + + static myIfUseIfTypeGuard: void; + } + + @NgModule({ + declarations: [MainComp, MyIf], + }) + export class MainModule {}` + }); + }); + + it('should narrow an *ngIf like directive with UseIf and != undefined', () => { + a({ + 'src/app.component.ts': '', + 'src/lib.ts': '', + 'src/app.module.ts': ` + import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core'; + + export interface Person { + name: string; + } + + @Component({ + selector: 'comp', + template: '
{{person.name}}
' + }) + export class MainComp { + person?: Person; + } + + export class MyIfContext { + public $implicit: any = null; + public myIf: any = null; + } + + @Directive({selector: '[myIf]'}) + export class MyIf { + constructor(templateRef: TemplateRef) {} + + @Input() + set myIf(condition: any) {} + + static myIfUseIfTypeGuard: void; + } + + @NgModule({ + declarations: [MainComp, MyIf], + }) + export class MainModule {}` + }); + }); }); describe('casting $any', () => { diff --git a/packages/compiler/src/aot/static_reflector.ts b/packages/compiler/src/aot/static_reflector.ts index 3576566945..13a57bedf9 100644 --- a/packages/compiler/src/aot/static_reflector.ts +++ b/packages/compiler/src/aot/static_reflector.ts @@ -30,6 +30,7 @@ const USE_VALUE = 'useValue'; const PROVIDE = 'provide'; const REFERENCE_SET = new Set([USE_VALUE, 'useFactory', 'data']); const TYPEGUARD_POSTFIX = 'TypeGuard'; +const USE_IF = 'UseIf'; function shouldIgnore(value: any): boolean { return value && value.__symbolic == 'ignore'; @@ -296,8 +297,17 @@ export class StaticReflector implements CompileReflector { const staticMembers = this._staticMembers(type); const result: {[key: string]: StaticSymbol} = {}; for (let name of staticMembers) { - result[name.substr(0, name.length - TYPEGUARD_POSTFIX.length)] = - this.getStaticSymbol(type.filePath, type.name, [name]); + if (name.endsWith(TYPEGUARD_POSTFIX)) { + let property = name.substr(0, name.length - TYPEGUARD_POSTFIX.length); + let value: any; + if (property.endsWith(USE_IF)) { + property = name.substr(0, property.length - USE_IF.length); + value = USE_IF; + } else { + value = this.getStaticSymbol(type.filePath, type.name, [name]); + } + result[property] = value; + } } return result; } diff --git a/packages/compiler/src/compiler_util/expression_converter.ts b/packages/compiler/src/compiler_util/expression_converter.ts index a828cd75ca..59aa5bdfd0 100644 --- a/packages/compiler/src/compiler_util/expression_converter.ts +++ b/packages/compiler/src/compiler_util/expression_converter.ts @@ -123,7 +123,7 @@ export function convertPropertyBinding( return new ConvertPropertyBindingResult([], outputExpr); } - stmts.push(currValExpr.set(outputExpr).toDeclStmt(null, [o.StmtModifier.Final])); + stmts.push(currValExpr.set(outputExpr).toDeclStmt(o.DYNAMIC_TYPE, [o.StmtModifier.Final])); return new ConvertPropertyBindingResult(stmts, currValExpr); } @@ -334,7 +334,13 @@ class _AstToIrVisitor implements cdAst.AstVisitor { } visitLiteralPrimitive(ast: cdAst.LiteralPrimitive, mode: _Mode): any { - return convertToStatementIfNeeded(mode, o.literal(ast.value)); + // For literal values of null, undefined, true, or false allow type inteference + // to infer the type. + const type = + ast.value === null || ast.value === undefined || ast.value === true || ast.value === true ? + o.INFERRED_TYPE : + undefined; + return convertToStatementIfNeeded(mode, o.literal(ast.value, type)); } private _getLocal(name: string): o.Expression|null { return this._localResolver.getLocal(name); } diff --git a/packages/compiler/src/view_compiler/type_check_compiler.ts b/packages/compiler/src/view_compiler/type_check_compiler.ts index 19b2221f7e..8d5bfeb6a7 100644 --- a/packages/compiler/src/view_compiler/type_check_compiler.ts +++ b/packages/compiler/src/view_compiler/type_check_compiler.ts @@ -58,6 +58,7 @@ export class TypeCheckCompiler { interface GuardExpression { guard: StaticSymbol; + useIf: boolean; expression: Expression; } @@ -127,8 +128,12 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { for (let input of directive.inputs) { const guard = directive.directive.guards[input.directiveName]; if (guard) { - result.push( - {guard, expression: {context: this.component, value: input.value} as Expression}); + const useIf = guard === 'UseIf'; + result.push({ + guard, + useIf, + expression: {context: this.component, value: input.value} as Expression + }); } } } @@ -178,8 +183,9 @@ class ViewBuilder implements TemplateAstVisitor, LocalResolver { nameResolver, o.variable(this.getOutputVar(context)), value, bindingId, BindingForm.TrySimple); if (stmts.length == 0) { - const callGuard = this.ctx.importExpr(guard.guard).callFn([currValExpr]); - guardExpression = guardExpression ? guardExpression.and(callGuard) : callGuard; + const guardClause = + guard.useIf ? currValExpr : this.ctx.importExpr(guard.guard).callFn([currValExpr]); + guardExpression = guardExpression ? guardExpression.and(guardClause) : guardClause; } } if (guardExpression) { diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 5262ad744e..07ccaf97dd 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -276,7 +276,6 @@ export declare class NgIf { ngIfElse: TemplateRef; ngIfThen: TemplateRef; constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef); - static ngIfTypeGuard: (v: T | null | undefined | false) => v is T; } /** @stable */