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:

committed by
Jason Aden

parent
e544742156
commit
e7d9cb3e4c
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user