fix(compiler-cli): type-check inputs that include undefined when there's coercion members (#38273)

For attribute bindings that target a directive's input, the template
type checker is able to verify that the type of the input expression is
compatible with the directive's declaration for said input. This
checking adheres to the `strictNullChecks` flag as configured in the
TypeScript compilation, such that errors are reported for expressions
that include `undefined` or `null` in their type if the input's
declaration does not include those types.

There was a bug with this level of type-checking for directives that
also declare coercion members, where binding an expression that includes
the `undefined` type to a directive's input that does not include the
`undefined` type would not be reported as error.

This commit fixes the bug by changing the type-constructor in type-check
code to use an intersection type of regular inputs and coerced inputs,
instead of a union type. The union type would inadvertently allow
`undefined` types to be assigned into the regular inputs, as that would
still satisfy the characteristics of a union type.

As a result of this change, you may start to see build failures if
`strictTemplates` is enabled and `strictInputTypes` is not disabled.
These errors are legitimate and some action is required to achieve a
successful build:

1. Update the templates for which an error is reported and introduce the
   non-null assertion operator at the end of the expression. This
   removes the `undefined` type from the expression's type, making it
   appear as a valid assignment.
2. Disable `strictNullInputTypes` in the compiler options. This will
   implicitly add the non-null assertion operators similar to option 1,
   but all templates in the compilation are affected.
3. Update the directive's input declaration to include the `undefined`
   type, if the directive is not implemented in an external library.

PR Close #38273
This commit is contained in:
JoostK 2020-07-28 20:16:57 +02:00 committed by Andrew Kushnir
parent 570d156ce4
commit 7525f3afc1
3 changed files with 36 additions and 3 deletions

View File

@ -166,8 +166,8 @@ function constructTypeCtorParameter(
if (coercedKeys.length > 0) {
const coercedLiteral = ts.createTypeLiteralNode(coercedKeys);
initType =
initType !== null ? ts.createUnionTypeNode([initType, coercedLiteral]) : coercedLiteral;
initType = initType !== null ? ts.createIntersectionTypeNode([initType, coercedLiteral]) :
coercedLiteral;
}
if (initType === null) {

View File

@ -179,7 +179,7 @@ TestClass.ngTypeCtor({value: 'test'});
const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!;
const ctorText = typeCtor.getText().replace(/[ \r\n]+/g, ' ');
expect(ctorText).toContain(
'init: Pick<TestClass, "foo"> | { bar: typeof TestClass.ngAcceptInputType_bar; }');
'init: Pick<TestClass, "foo"> & { bar: typeof TestClass.ngAcceptInputType_bar; }');
});
});
});

View File

@ -1498,6 +1498,39 @@ export declare class AnimationEvent {
expect(diags[0].messageText)
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
});
it('should give an error for undefined bindings into regular inputs when coercion members are present',
() => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
import {Component, Directive, NgModule, Input} from '@angular/core';
@Component({
selector: 'blah',
template: '<input dir [regular]="undefined" [coerced]="1">',
})
export class FooCmp {
invalidType = true;
}
@Directive({selector: '[dir]'})
export class CoercionDir {
@Input() regular: string;
@Input() coerced: boolean;
static ngAcceptInputType_coerced: boolean|number;
}
@NgModule({
declarations: [FooCmp, CoercionDir],
})
export class FooModule {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toBe(`Type 'undefined' is not assignable to type 'string'.`);
});
});
describe('legacy schema checking with the DOM schema', () => {