From 79141f442404f60e620a767330948bebd4881915 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 24 Apr 2019 11:31:16 -0700 Subject: [PATCH] fix(ivy): generate default 'any' types for type ctor generic params (#30094) ngtsc generates type constructors which infer the type of a directive based on its inputs. Previously, a bug existed where this inference would fail in the case of 'any' input values. For example, the inference of NgForOf fails when an 'any' is provided, as it causes TypeScript to attempt to solve: T[] = any In this case, T gets inferred as {}, the empty object type, which is not desirable. The fix is to assign generic types in type constructors a default type of 'any', which TypeScript uses instead of {} when inference fails. PR Close #30094 --- .../ngtsc/typecheck/src/type_constructor.ts | 70 ++++++++++++++++++- .../test/ngtsc/template_typecheck_spec.ts | 23 ++++++ 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts index c308ebfd7a..1d523ff9ef 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_constructor.ts @@ -25,9 +25,11 @@ export function generateTypeCtorDeclarationFn( const initParam = constructTypeCtorParameter(node, meta, rawType); + const typeParameters = typeParametersWithDefaultTypes(node.typeParameters); + if (meta.body) { const fnType = ts.createFunctionTypeNode( - /* typeParameters */ node.typeParameters, + /* typeParameters */ typeParameters, /* parameters */[initParam], /* type */ rawType, ); @@ -45,7 +47,7 @@ export function generateTypeCtorDeclarationFn( /* modifiers */[ts.createModifier(ts.SyntaxKind.DeclareKeyword)], /* asteriskToken */ undefined, /* name */ meta.fnName, - /* typeParameters */ node.typeParameters, + /* typeParameters */ typeParameters, /* parameters */[initParam], /* type */ rawType, /* body */ undefined); @@ -108,7 +110,7 @@ export function generateInlineTypeCtor( /* asteriskToken */ undefined, /* name */ meta.fnName, /* questionToken */ undefined, - /* typeParameters */ node.typeParameters, + /* typeParameters */ typeParametersWithDefaultTypes(node.typeParameters), /* parameters */[initParam], /* type */ rawType, /* body */ body, ); @@ -168,3 +170,65 @@ export function requiresInlineTypeCtor(node: ClassDeclaration { + * ngForOf: T[]; + * } + * + * declare function ctor(o: Partial, 'ngForOf'>>): NgFor; + * ``` + * + * An invocation looks like: + * + * ``` + * var _t1 = ctor({ngForOf: [1, 2]}); + * ``` + * + * This correctly infers the type `NgFor` for `_t1`, since `T` is inferred from the + * assignment of type `number[]` to `ngForOf`'s type `T[]`. However, if `any` is passed instead: + * + * ``` + * var _t2 = ctor({ngForOf: [1, 2] as any}); + * ``` + * + * then inference for `T` fails (it cannot be inferred from `T[] = any`). In this case, `T` takes + * the type `{}`, and so `_t2` is inferred as `NgFor<{}>`. This is obviously wrong. + * + * Adding a default type to the generic declaration in the constructor solves this problem, as the + * default type will be used in the event that inference fails. + * + * ``` + * declare function ctor(o: Partial, 'ngForOf'>>): NgFor; + * + * var _t3 = ctor({ngForOf: [1, 2] as any}); + * ``` + * + * This correctly infers `T` as `any`, and therefore `_t3` as `NgFor`. + */ +function typeParametersWithDefaultTypes( + params: ReadonlyArray| undefined): ts.TypeParameterDeclaration[]| + undefined { + if (params === undefined) { + return undefined; + } + + return params.map(param => { + if (param.default === undefined) { + return ts.updateTypeParameterDeclaration( + /* node */ param, + /* name */ param.name, + /* constraint */ param.constraint, + /* defaultType */ ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + } else { + return param; + } + }); +} diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index e8e55e2bc3..e4f86e318d 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -148,6 +148,29 @@ describe('ngtsc type checking', () => { expect(diags[0].messageText).toContain('does_not_exist'); }); + it('should accept an NgFor iteration over an any-typed value', () => { + env.write('test.ts', ` + import {CommonModule} from '@angular/common'; + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
{{user.name}}
', + }) + export class TestCmp { + users: any; + } + + @NgModule({ + declarations: [TestCmp], + imports: [CommonModule], + }) + export class Module {} + `); + + env.driveMain(); + }); + it('should report an error with pipe bindings', () => { env.write('test.ts', ` import {CommonModule} from '@angular/common';