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';