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
This commit is contained in:
Alex Rickabaugh 2019-04-24 11:31:16 -07:00 committed by Andrew Kushnir
parent 28fd5ab12b
commit 79141f4424
2 changed files with 90 additions and 3 deletions

View File

@ -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<ts.ClassDeclaratio
// The class requires an inline type constructor if it has constrained (bound) generics.
return !checkIfGenericTypesAreUnbound(node);
}
/**
* Add a default `= any` to type parameters that don't have a default value already.
*
* TypeScript uses the default type of a type parameter whenever inference of that parameter fails.
* This can happen when inferring a complex type from 'any'. For example, if `NgFor`'s inference is
* done with the TCB code:
*
* ```
* class NgFor<T> {
* ngForOf: T[];
* }
*
* declare function ctor<T>(o: Partial<Pick<NgFor<T>, 'ngForOf'>>): NgFor<T>;
* ```
*
* An invocation looks like:
*
* ```
* var _t1 = ctor({ngForOf: [1, 2]});
* ```
*
* This correctly infers the type `NgFor<number>` 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<T = any>(o: Partial<Pick<NgFor<T>, 'ngForOf'>>): NgFor<T>;
*
* var _t3 = ctor({ngForOf: [1, 2] as any});
* ```
*
* This correctly infers `T` as `any`, and therefore `_t3` as `NgFor<any>`.
*/
function typeParametersWithDefaultTypes(
params: ReadonlyArray<ts.TypeParameterDeclaration>| 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;
}
});
}

View File

@ -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: '<div *ngFor="let user of users">{{user.name}}</div>',
})
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';