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

@ -44,7 +44,8 @@ export class DirectiveResolver {
const metadata = findLast(typeMetadata, isDirectiveMetadata);
if (metadata) {
const propertyMetadata = this._reflector.propMetadata(type);
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, type);
const guards = this._reflector.guards(type);
return this._mergeWithPropertyMetadata(metadata, propertyMetadata, guards, type);
}
}
@ -56,12 +57,12 @@ export class DirectiveResolver {
}
private _mergeWithPropertyMetadata(
dm: Directive, propertyMetadata: {[key: string]: any[]}, directiveType: Type): Directive {
dm: Directive, propertyMetadata: {[key: string]: any[]}, guards: {[key: string]: any},
directiveType: Type): Directive {
const inputs: string[] = [];
const outputs: string[] = [];
const host: {[key: string]: string} = {};
const queries: {[key: string]: any} = {};
Object.keys(propertyMetadata).forEach((propName: string) => {
const input = findLast(propertyMetadata[propName], (a) => createInput.isTypeOf(a));
if (input) {
@ -105,18 +106,20 @@ export class DirectiveResolver {
queries[propName] = query;
}
});
return this._merge(dm, inputs, outputs, host, queries, directiveType);
return this._merge(dm, inputs, outputs, host, queries, guards, directiveType);
}
private _extractPublicName(def: string) { return splitAtColon(def, [null !, def])[1].trim(); }
private _dedupeBindings(bindings: string[]): string[] {
const names = new Set<string>();
const publicNames = new Set<string>();
const reversedResult: string[] = [];
// go last to first to allow later entries to overwrite previous entries
for (let i = bindings.length - 1; i >= 0; i--) {
const binding = bindings[i];
const name = this._extractPublicName(binding);
publicNames.add(name);
if (!names.has(name)) {
names.add(name);
reversedResult.push(binding);
@ -127,14 +130,13 @@ export class DirectiveResolver {
private _merge(
directive: Directive, inputs: string[], outputs: string[], host: {[key: string]: string},
queries: {[key: string]: any}, directiveType: Type): Directive {
queries: {[key: string]: any}, guards: {[key: string]: any}, directiveType: Type): Directive {
const mergedInputs =
this._dedupeBindings(directive.inputs ? directive.inputs.concat(inputs) : inputs);
const mergedOutputs =
this._dedupeBindings(directive.outputs ? directive.outputs.concat(outputs) : outputs);
const mergedHost = directive.host ? {...directive.host, ...host} : host;
const mergedQueries = directive.queries ? {...directive.queries, ...queries} : queries;
if (createComponent.isTypeOf(directive)) {
const comp = directive as Component;
return createComponent({
@ -166,7 +168,7 @@ export class DirectiveResolver {
host: mergedHost,
exportAs: directive.exportAs,
queries: mergedQueries,
providers: directive.providers
providers: directive.providers, guards
});
}
}