feat(ivy): disable strict null checks for input bindings (#33066)

This commit introduces an internal config option of the template type
checker that allows to disable strict null checks of input bindings to
directives. This may be particularly useful when a directive is from a
library that is not compiled with `strictNullChecks` enabled.

Right now, strict null checks are enabled when  `fullTemplateTypeCheck`
is turned on, and disabled when it's off. In the near future, several of
the internal configuration options will be added as public Angular
compiler options so that users can have fine-grained control over which
areas of the template type checker to enable, allowing for a more
incremental migration strategy.

PR Close #33066
This commit is contained in:
JoostK 2019-10-11 19:41:05 +02:00 committed by Miško Hevery
parent 50bf17aca0
commit ece0b2d7ce
5 changed files with 53 additions and 9 deletions

View File

@ -401,6 +401,7 @@ export class NgtscProgram implements api.Program {
checkQueries: false, checkQueries: false,
checkTemplateBodies: true, checkTemplateBodies: true,
checkTypeOfInputBindings: true, checkTypeOfInputBindings: true,
strictNullInputBindings: true,
// Even in full template type-checking mode, DOM binding checks are not quite ready yet. // Even in full template type-checking mode, DOM binding checks are not quite ready yet.
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
checkTypeOfPipes: true, checkTypeOfPipes: true,
@ -412,6 +413,7 @@ export class NgtscProgram implements api.Program {
checkQueries: false, checkQueries: false,
checkTemplateBodies: false, checkTemplateBodies: false,
checkTypeOfInputBindings: false, checkTypeOfInputBindings: false,
strictNullInputBindings: false,
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
checkTypeOfPipes: false, checkTypeOfPipes: false,
strictSafeNavigationTypes: false, strictSafeNavigationTypes: false,

View File

@ -83,6 +83,18 @@ export interface TypeCheckingConfig {
*/ */
checkTypeOfInputBindings: boolean; checkTypeOfInputBindings: boolean;
/**
* Whether to use strict null types for input bindings for directives.
*
* If this is `true`, applications that are compiled with TypeScript's `strictNullChecks` enabled
* will produce type errors for bindings which can evaluate to `undefined` or `null` where the
* inputs's type does not include `undefined` or `null` in its type. If set to `false`, all
* binding expressions are wrapped in a non-null assertion operator to effectively disable strict
* null checks. This may be particularly useful when the directive is from a library that is not
* compiled with `strictNullChecks` enabled.
*/
strictNullInputBindings: boolean;
/** /**
* Whether to check the left-hand side type of binding operations to DOM properties. * Whether to check the left-hand side type of binding operations to DOM properties.
* *

View File

@ -388,11 +388,14 @@ class TcbUnclaimedInputsOp extends TcbOp {
let expr = tcbExpression( let expr = tcbExpression(
binding.value, this.tcb, this.scope, binding.valueSpan || binding.sourceSpan); binding.value, this.tcb, this.scope, binding.valueSpan || binding.sourceSpan);
// If checking the type of bindings is disabled, cast the resulting expression to 'any' before
// the assignment.
if (!this.tcb.env.config.checkTypeOfInputBindings) { if (!this.tcb.env.config.checkTypeOfInputBindings) {
// If checking the type of bindings is disabled, cast the resulting expression to 'any'
// before the assignment.
expr = tsCastToAny(expr); expr = tsCastToAny(expr);
} else if (!this.tcb.env.config.strictNullInputBindings) {
// If strict null checks are disabled, erase `null` and `undefined` from the type by
// wrapping the expression in a non-null assertion.
expr = ts.createNonNullExpression(expr);
} }
if (this.tcb.env.config.checkTypeOfDomBindings && binding.type === BindingType.Property) { if (this.tcb.env.config.checkTypeOfDomBindings && binding.type === BindingType.Property) {
@ -781,11 +784,18 @@ function tcbCallTypeCtor(
const members = inputs.map(input => { const members = inputs.map(input => {
if (input.type === 'binding') { if (input.type === 'binding') {
// For bound inputs, the property is assigned the binding expression. // For bound inputs, the property is assigned the binding expression.
let expression = input.expression; let expr = input.expression;
if (!tcb.env.config.checkTypeOfInputBindings) { if (!tcb.env.config.checkTypeOfInputBindings) {
expression = tsCastToAny(expression); // If checking the type of bindings is disabled, cast the resulting expression to 'any'
// before the assignment.
expr = tsCastToAny(expr);
} else if (!tcb.env.config.strictNullInputBindings) {
// If strict null checks are disabled, erase `null` and `undefined` from the type by
// wrapping the expression in a non-null assertion.
expr = ts.createNonNullExpression(expr);
} }
const assignment = ts.createPropertyAssignment(input.field, wrapForDiagnostics(expression));
const assignment = ts.createPropertyAssignment(input.field, wrapForDiagnostics(expr));
addParseSpanInfo(assignment, input.sourceSpan); addParseSpanInfo(assignment, input.sourceSpan);
return assignment; return assignment;
} else { } else {

View File

@ -119,6 +119,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
checkQueries: false, checkQueries: false,
checkTemplateBodies: true, checkTemplateBodies: true,
checkTypeOfInputBindings: true, checkTypeOfInputBindings: true,
strictNullInputBindings: true,
// Feature is still in development. // Feature is still in development.
// TODO(alxhub): enable when DOM checking via lib.dom.d.ts is further along. // TODO(alxhub): enable when DOM checking via lib.dom.d.ts is further along.
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
@ -160,6 +161,7 @@ export function tcb(
applyTemplateContextGuards: true, applyTemplateContextGuards: true,
checkQueries: false, checkQueries: false,
checkTypeOfInputBindings: true, checkTypeOfInputBindings: true,
strictNullInputBindings: true,
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
checkTypeOfPipes: true, checkTypeOfPipes: true,
checkTemplateBodies: true, checkTemplateBodies: true,

View File

@ -222,6 +222,7 @@ describe('type check blocks', () => {
checkQueries: false, checkQueries: false,
checkTemplateBodies: true, checkTemplateBodies: true,
checkTypeOfInputBindings: true, checkTypeOfInputBindings: true,
strictNullInputBindings: true,
checkTypeOfDomBindings: false, checkTypeOfDomBindings: false,
checkTypeOfPipes: true, checkTypeOfPipes: true,
strictSafeNavigationTypes: true, strictSafeNavigationTypes: true,
@ -257,20 +258,37 @@ describe('type check blocks', () => {
}); });
}); });
describe('config.strictNullInputBindings', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
it('should include null and undefined when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('Dir.ngTypeCtor({ dirInput: ((ctx).a) })');
expect(block).toContain('(ctx).b;');
});
it('should use the non-null assertion operator when disabled', () => {
const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, strictNullInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('Dir.ngTypeCtor({ dirInput: ((ctx).a!) })');
expect(block).toContain('(ctx).b!;');
});
});
describe('config.checkTypeOfBindings', () => { describe('config.checkTypeOfBindings', () => {
const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="a"></div>`; const TEMPLATE = `<div dir [dirInput]="a" [nonDirInput]="b"></div>`;
it('should check types of bindings when enabled', () => { it('should check types of bindings when enabled', () => {
const block = tcb(TEMPLATE, DIRECTIVES); const block = tcb(TEMPLATE, DIRECTIVES);
expect(block).toContain('Dir.ngTypeCtor({ dirInput: ((ctx).a) })'); expect(block).toContain('Dir.ngTypeCtor({ dirInput: ((ctx).a) })');
expect(block).toContain('(ctx).a;'); expect(block).toContain('(ctx).b;');
}); });
it('should not check types of bindings when disabled', () => { it('should not check types of bindings when disabled', () => {
const DISABLED_CONFIG: const DISABLED_CONFIG:
TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false}; TypeCheckingConfig = {...BASE_CONFIG, checkTypeOfInputBindings: false};
const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG); const block = tcb(TEMPLATE, DIRECTIVES, DISABLED_CONFIG);
expect(block).toContain('Dir.ngTypeCtor({ dirInput: (((ctx).a as any)) })'); expect(block).toContain('Dir.ngTypeCtor({ dirInput: (((ctx).a as any)) })');
expect(block).toContain('((ctx).a as any);'); expect(block).toContain('((ctx).b as any);');
}); });
}); });