fix(ivy): add strictLiteralTypes to align Ivy + VE checking of literals (#35462)

Under View Engine's default (non-fullTemplateTypeCheck) checking, object and
array literals which appear in templates are treated as having type `any`.
This allows a number of patterns which would not otherwise compile, such as
indexing an object literal by a string:

```html
{{ {'a': 1, 'b': 2}[value] }}
```

(where `value` is `string`)

Ivy, meanwhile, has always inferred strong types for object literals, even
in its compatibility mode. This commit fixes the bug, and adds the
`strictLiteralTypes` flag to specifically control this inference. When the
flag is `false` (in compatibility mode), object and array literals receive
the `any` type.

PR Close #35462
This commit is contained in:
Alex Rickabaugh 2020-02-14 13:30:43 -08:00 committed by Miško Hevery
parent a61fe4177f
commit 4253662231
7 changed files with 33 additions and 2 deletions

View File

@ -121,6 +121,7 @@ In case of a false positive like these, there are a few options:
|`strictOutputEventTypes`|Whether `$event` will have the correct type for event bindings to component/directive an `@Output()`, or to animation events. If disabled, it will be `any`.|
|`strictDomEventTypes`|Whether `$event` will have the correct type for event bindings to DOM events. If disabled, it will be `any`.|
|`strictContextGenerics`|Whether the type parameters of generic components will be inferred correctly (including any generic bounds). If disabled, any type parameters will be `any`.|
|`strictLiteralTypes`|Whether object and array literals declared in the template will have their type inferred. If disabled, the type of such literals will be `any`.|
If you still have issues after troubleshooting with these flags, you can fall back to full mode by disabling `strictTemplates`.

View File

@ -278,6 +278,14 @@ export interface StrictTemplateOptions {
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictContextGenerics?: boolean;
/**
* Whether object or array literals defined in templates use their inferred type, or are
* interpreted as `any`.
*
* Defaults to `false` unless `fullTemplateTypeCheck` or `strictTemplates` are set.
*/
strictLiteralTypes?: boolean;
}
/**

View File

@ -424,6 +424,7 @@ export class NgCompiler {
checkTypeOfPipes: true,
strictSafeNavigationTypes: strictTemplates,
useContextGenericType: strictTemplates,
strictLiteralTypes: true,
};
} else {
typeCheckingConfig = {
@ -442,6 +443,7 @@ export class NgCompiler {
checkTypeOfPipes: false,
strictSafeNavigationTypes: false,
useContextGenericType: false,
strictLiteralTypes: false,
};
}
@ -473,6 +475,9 @@ export class NgCompiler {
if (this.options.strictContextGenerics !== undefined) {
typeCheckingConfig.useContextGenericType = this.options.strictContextGenerics;
}
if (this.options.strictLiteralTypes !== undefined) {
typeCheckingConfig.strictLiteralTypes = this.options.strictLiteralTypes;
}
// Execute the typeCheck phase of each decorator in the program.
const prepSpan = this.perfRecorder.start('typeCheckPrep');

View File

@ -220,6 +220,15 @@ export interface TypeCheckingConfig {
* component will be set to `any` during type-checking.
*/
useContextGenericType: boolean;
/**
* Whether or not to infer types for object and array literals in the template.
*
* If this is `true`, then the type of an object or an array literal in the template will be the
* same type that TypeScript would infer if the literal appeared in code. If `false`, then such
* literals are cast to `any` when declared.
*/
strictLiteralTypes: boolean;
}

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {TypeCheckingConfig} from './api';
import {addParseSpanInfo, ignoreDiagnostics, wrapForDiagnostics} from './diagnostics';
import {tsCastToAny} from './ts_util';
export const NULL_AS_ANY =
ts.createAsExpression(ts.createNull(), ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
@ -143,7 +144,9 @@ class AstTranslator implements AstVisitor {
visitLiteralArray(ast: LiteralArray): ts.Expression {
const elements = ast.expressions.map(expr => this.translate(expr));
const node = ts.createArrayLiteral(elements);
const literal = ts.createArrayLiteral(elements);
// If strictLiteralTypes is disabled, array literals are cast to `any`.
const node = this.config.strictLiteralTypes ? literal : tsCastToAny(literal);
addParseSpanInfo(node, ast.sourceSpan);
return node;
}
@ -153,7 +156,9 @@ class AstTranslator implements AstVisitor {
const value = this.translate(ast.values[idx]);
return ts.createPropertyAssignment(ts.createStringLiteral(key), value);
});
const node = ts.createObjectLiteral(properties, true);
const literal = ts.createObjectLiteral(properties, true);
// If strictLiteralTypes is disabled, object literals are cast to `any`.
const node = this.config.strictLiteralTypes ? literal : tsCastToAny(literal);
addParseSpanInfo(node, ast.sourceSpan);
return node;
}

View File

@ -164,6 +164,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = {
checkTypeOfPipes: true,
strictSafeNavigationTypes: true,
useContextGenericType: true,
strictLiteralTypes: true,
};
// Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead.
@ -219,6 +220,7 @@ export function tcb(
checkTemplateBodies: true,
strictSafeNavigationTypes: true,
useContextGenericType: true,
strictLiteralTypes: true,
};
options = options || {
emitSpans: false,

View File

@ -330,6 +330,7 @@ describe('type check blocks', () => {
checkTypeOfPipes: true,
strictSafeNavigationTypes: true,
useContextGenericType: true,
strictLiteralTypes: true,
};
describe('config.applyTemplateContextGuards', () => {