mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
fix(compiler-cli): ensure literal types are retained when strictNullInputTypes is disabled (#38305)
Consider the `NgModel` directive which has the `ngModelOptions` input:
```ts
class NgModel {
@Input() ngModelOptions: { updateOn: 'blur'|'change'|'submit' };
}
```
In a template this may be set using an object literal as follows:
```html
<input ngModel [ngModelOptions]="{updateOn: 'blur'}">
```
This assignment should be accepted, as the object's type aligns with the
`ngModelOptions` input in `NgModel`. However, if the `strictNullInputTypes`
option is disabled this assignment would inadvertently produce an error:
```
Type '{ updateOn: string; }' is not assignable to type '{ updateOn: "blur"|"change"|"submit"; }'.
Types of property 'updateOn' are incompatible.
Type 'string' is not assignable to type '"blur"|"change"|"submit"'
```
This is due to the `'blur'` value being inferred to be of type `string`
instead of retaining its literal type. The non-null assertion operator
that is automatically inserted for input binding assignments when
`strictNullInputTypes` is disabled inhibits TypeScript from inferring
the string value as its literal type.
This commit fixes the issue by omitting the insertion of the non-null
operator for object literals and array literals.
PR Close #38305
This commit is contained in:
@@ -701,16 +701,7 @@ class TcbDirectiveInputsOp extends TcbOp {
|
||||
const inputs = getBoundInputs(this.dir, this.node, this.tcb);
|
||||
for (const input of inputs) {
|
||||
// For bound inputs, the property is assigned the binding expression.
|
||||
let expr = translateInput(input.attribute, this.tcb, this.scope);
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
const expr = widenBinding(translateInput(input.attribute, this.tcb, this.scope), this.tcb);
|
||||
|
||||
let assignment: ts.Expression = wrapForDiagnostics(expr);
|
||||
|
||||
@@ -922,16 +913,7 @@ class TcbUnclaimedInputsOp extends TcbOp {
|
||||
continue;
|
||||
}
|
||||
|
||||
let expr = tcbExpression(binding.value, this.tcb, this.scope);
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
const expr = widenBinding(tcbExpression(binding.value, this.tcb, this.scope), this.tcb);
|
||||
|
||||
if (this.tcb.env.config.checkTypeOfDomBindings && binding.type === BindingType.Property) {
|
||||
if (binding.name !== 'style' && binding.name !== 'class') {
|
||||
@@ -1811,16 +1793,7 @@ function tcbCallTypeCtor(
|
||||
|
||||
if (input.type === 'binding') {
|
||||
// For bound inputs, the property is assigned the binding expression.
|
||||
let expr = input.expression;
|
||||
if (!tcb.env.config.checkTypeOfInputBindings) {
|
||||
// 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 expr = widenBinding(input.expression, tcb);
|
||||
|
||||
const assignment = ts.createPropertyAssignment(propertyName, wrapForDiagnostics(expr));
|
||||
addParseSpanInfo(assignment, input.sourceSpan);
|
||||
@@ -1883,6 +1856,31 @@ function translateInput(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Potentially widens the type of `expr` according to the type-checking configuration.
|
||||
*/
|
||||
function widenBinding(expr: ts.Expression, tcb: Context): ts.Expression {
|
||||
if (!tcb.env.config.checkTypeOfInputBindings) {
|
||||
// If checking the type of bindings is disabled, cast the resulting expression to 'any'
|
||||
// before the assignment.
|
||||
return tsCastToAny(expr);
|
||||
} else if (!tcb.env.config.strictNullInputBindings) {
|
||||
if (ts.isObjectLiteralExpression(expr) || ts.isArrayLiteralExpression(expr)) {
|
||||
// Object literals and array literals should not be wrapped in non-null assertions as that
|
||||
// would cause literals to be prematurely widened, resulting in type errors when assigning
|
||||
// into a literal type.
|
||||
return expr;
|
||||
} else {
|
||||
// If strict null checks are disabled, erase `null` and `undefined` from the type by
|
||||
// wrapping the expression in a non-null assertion.
|
||||
return ts.createNonNullExpression(expr);
|
||||
}
|
||||
} else {
|
||||
// No widening is requested, use the expression as is.
|
||||
return expr;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An input binding that corresponds with a field of a directive.
|
||||
*/
|
||||
|
||||
@@ -277,6 +277,46 @@ runInEachFileSystem(() => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should retain literal types in object literals together if strictNullInputBindings is disabled',
|
||||
() => {
|
||||
const messages = diagnose(
|
||||
`<div dir [ngModelOptions]="{updateOn: 'change'}"></div>`, `
|
||||
class Dir {
|
||||
ngModelOptions: { updateOn: 'change'|'blur' };
|
||||
}
|
||||
|
||||
class TestComponent {}`,
|
||||
[{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {'ngModelOptions': 'ngModelOptions'},
|
||||
}],
|
||||
[], {strictNullInputBindings: false});
|
||||
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should retain literal types in array literals together if strictNullInputBindings is disabled',
|
||||
() => {
|
||||
const messages = diagnose(
|
||||
`<div dir [options]="['literal']"></div>`, `
|
||||
class Dir {
|
||||
options!: Array<'literal'>;
|
||||
}
|
||||
|
||||
class TestComponent {}`,
|
||||
[{
|
||||
type: 'directive',
|
||||
name: 'Dir',
|
||||
selector: '[dir]',
|
||||
inputs: {'options': 'options'},
|
||||
}],
|
||||
[], {strictNullInputBindings: false});
|
||||
|
||||
expect(messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not produce diagnostics for user code', () => {
|
||||
const messages = diagnose(`{{ person.name }}`, `
|
||||
class TestComponent {
|
||||
|
||||
@@ -46,6 +46,7 @@ export function typescriptLibDts(): TestFile {
|
||||
[index: number]: T;
|
||||
length: number;
|
||||
}
|
||||
declare interface Iterable<T> {}
|
||||
declare interface String {
|
||||
length: number;
|
||||
}
|
||||
@@ -63,7 +64,7 @@ export function typescriptLibDts(): TestFile {
|
||||
}
|
||||
declare interface HTMLElement {
|
||||
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any): void;
|
||||
addEventListener(type: string, listener: (evt: Event): void;): void;
|
||||
addEventListener(type: string, listener: (evt: Event) => void): void;
|
||||
}
|
||||
declare interface HTMLDivElement extends HTMLElement {}
|
||||
declare interface HTMLImageElement extends HTMLElement {
|
||||
@@ -94,8 +95,8 @@ export function angularCoreDts(): TestFile {
|
||||
name: absoluteFrom('/node_modules/@angular/core/index.d.ts'),
|
||||
contents: `
|
||||
export declare class TemplateRef<C> {
|
||||
abstract readonly elementRef: unknown;
|
||||
abstract createEmbeddedView(context: C): unknown;
|
||||
readonly elementRef: unknown;
|
||||
createEmbeddedView(context: C): unknown;
|
||||
}
|
||||
|
||||
export declare class EventEmitter<T> {
|
||||
|
||||
Reference in New Issue
Block a user