feat(compiler): narrow types of expressions used in *ngIf (#20702)

Structural directives can now specify a type guard that describes
what types can be inferred for an input expression inside the
directive's template.

NgIf was modified to declare an input guard on ngIf.

After this change, `fullTemplateTypeCheck` will infer that
usage of `ngIf` expression inside it's template is truthy.

For example, if a component has a property `person?: Person`
and a template of `<div *ngIf="person"> {{person.name}} </div>`
the compiler will no longer report that `person` might be null or
undefined.

The template compiler will generate code similar to,

```
  if (NgIf.ngIfTypeGuard(instance.person)) {
    instance.person.name
  }
```

to validate the template's use of the interpolation expression.
Calling the type guard in this fashion allows TypeScript to infer
that `person` is non-null.

Fixes: #19756?

PR Close #20702
This commit is contained in:
Chuck Jazdzewski
2017-11-29 16:29:05 -08:00
committed by Jason Aden
parent e544742156
commit e7d9cb3e4c
19 changed files with 341 additions and 53 deletions

View File

@ -271,7 +271,7 @@ export class AotCompiler {
const {template: parsedTemplate, pipes: usedPipes} =
this._parseTemplate(compMeta, moduleMeta, directives);
ctx.statements.push(...this._typeCheckCompiler.compileComponent(
componentId, compMeta, parsedTemplate, usedPipes, externalReferenceVars));
componentId, compMeta, parsedTemplate, usedPipes, externalReferenceVars, ctx));
}
emitMessageBundle(analyzeResult: NgAnalyzedModules, locale: string|null): MessageBundle {

View File

@ -29,6 +29,7 @@ const IGNORE = {
const USE_VALUE = 'useValue';
const PROVIDE = 'provide';
const REFERENCE_SET = new Set([USE_VALUE, 'useFactory', 'data']);
const TYPEGUARD_POSTFIX = 'TypeGuard';
function shouldIgnore(value: any): boolean {
return value && value.__symbolic == 'ignore';
@ -43,6 +44,7 @@ export class StaticReflector implements CompileReflector {
private propertyCache = new Map<StaticSymbol, {[key: string]: any[]}>();
private parameterCache = new Map<StaticSymbol, any[]>();
private methodCache = new Map<StaticSymbol, {[key: string]: boolean}>();
private staticCache = new Map<StaticSymbol, string[]>();
private conversionMap = new Map<StaticSymbol, (context: StaticSymbol, args: any[]) => any>();
private injectionToken: StaticSymbol;
private opaqueToken: StaticSymbol;
@ -251,6 +253,18 @@ export class StaticReflector implements CompileReflector {
return methodNames;
}
private _staticMembers(type: StaticSymbol): string[] {
let staticMembers = this.staticCache.get(type);
if (!staticMembers) {
const classMetadata = this.getTypeMetadata(type);
const staticMemberData = classMetadata['statics'] || {};
staticMembers = Object.keys(staticMemberData);
this.staticCache.set(type, staticMembers);
}
return staticMembers;
}
private findParentType(type: StaticSymbol, classMetadata: any): StaticSymbol|undefined {
const parentType = this.trySimplify(type, classMetadata['extends']);
if (parentType instanceof StaticSymbol) {
@ -273,6 +287,21 @@ export class StaticReflector implements CompileReflector {
}
}
guards(type: any): {[key: string]: StaticSymbol} {
if (!(type instanceof StaticSymbol)) {
this.reportError(
new Error(`guards received ${JSON.stringify(type)} which is not a StaticSymbol`), type);
return {};
}
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]);
}
return result;
}
private _registerDecoratorOrConstructor(type: StaticSymbol, ctor: any): void {
this.conversionMap.set(type, (context: StaticSymbol, args: any[]) => new ctor(...args));
}