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:
parent
570d156ce4
commit
7525f3afc1
|
@ -166,8 +166,8 @@ function constructTypeCtorParameter(
|
||||||
if (coercedKeys.length > 0) {
|
if (coercedKeys.length > 0) {
|
||||||
const coercedLiteral = ts.createTypeLiteralNode(coercedKeys);
|
const coercedLiteral = ts.createTypeLiteralNode(coercedKeys);
|
||||||
|
|
||||||
initType =
|
initType = initType !== null ? ts.createIntersectionTypeNode([initType, coercedLiteral]) :
|
||||||
initType !== null ? ts.createUnionTypeNode([initType, coercedLiteral]) : coercedLiteral;
|
coercedLiteral;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initType === null) {
|
if (initType === null) {
|
||||||
|
|
|
@ -179,7 +179,7 @@ TestClass.ngTypeCtor({value: 'test'});
|
||||||
const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!;
|
const typeCtor = TestClassWithCtor.members.find(isTypeCtor)!;
|
||||||
const ctorText = typeCtor.getText().replace(/[ \r\n]+/g, ' ');
|
const ctorText = typeCtor.getText().replace(/[ \r\n]+/g, ' ');
|
||||||
expect(ctorText).toContain(
|
expect(ctorText).toContain(
|
||||||
'init: Pick<TestClass, "foo"> | { bar: typeof TestClass.ngAcceptInputType_bar; }');
|
'init: Pick<TestClass, "foo"> & { bar: typeof TestClass.ngAcceptInputType_bar; }');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1498,6 +1498,39 @@ export declare class AnimationEvent {
|
||||||
expect(diags[0].messageText)
|
expect(diags[0].messageText)
|
||||||
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
|
.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', () => {
|
describe('legacy schema checking with the DOM schema', () => {
|
||||||
|
|
Loading…
Reference in New Issue