fix(core): allow proper type inference when `ngFor` is used with a `trackBy` function (#42692)

In #41995 the type of `TrackByFunction` was changed such that the
declaration of a `trackBy` function did not cause the item type to be
widened to the `trackBy`'s item type, which may be a supertype of the
iterated type. This has introduced situations where the template type
checker is now reporting errors for cases where a `trackBy` function is
no longer assignable to `TrackByFunction`.

This commit fixes the error by also including the item type `T` in
addition to the constrained type parameter `U`, allowing TypeScript to
infer an appropriate `T`.

Fixes #42609

PR Close #42692
This commit is contained in:
JoostK 2021-06-28 21:35:03 +02:00 committed by Andrew Kushnir
parent 970d7f7c6f
commit 51156f3f07
4 changed files with 75 additions and 3 deletions

View File

@ -1258,7 +1258,7 @@ export class TestabilityRegistry {
// @public // @public
export interface TrackByFunction<T> { export interface TrackByFunction<T> {
// (undocumented) // (undocumented)
<U extends T>(index: number, item: U): any; <U extends T>(index: number, item: T & U): any;
} }
// @public // @public

View File

@ -116,5 +116,5 @@ export interface OnDestroy {
} }
export interface TrackByFunction<T> { export interface TrackByFunction<T> {
<U extends T>(index: number, item: U): any; <U extends T>(index: number, item: T&U): any;
} }

View File

@ -1086,6 +1086,78 @@ export declare class AnimationEvent {
env.driveMain(); env.driveMain();
}); });
// https://github.com/angular/angular/issues/42609
it('should accept NgFor iteration when trackBy is used with an `any` array', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
interface ItemType {
id: string;
}
@Component({
selector: 'test',
template: '<div *ngFor="let item of anyList; trackBy: trackByBase">{{item.name}}</div>',
})
class TestCmp {
anyList!: any[];
trackByBase(index: number, item: ItemType): string {
return item.id;
}
}
@NgModule({
declarations: [TestCmp],
imports: [CommonModule],
})
class Module {}
`);
env.driveMain();
});
it('should reject NgFor iteration when trackBy is incompatible with item type', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
interface ItemType {
id: string;
}
interface UnrelatedType {
name: string;
}
@Component({
selector: 'test',
template: '<div *ngFor="let item of unrelatedList; trackBy: trackByBase">{{item.name}}</div>',
})
class TestCmp {
unrelatedList!: UnrelatedType[];
trackByBase(index: number, item: ItemType): string {
return item.id;
}
}
@NgModule({
declarations: [TestCmp],
imports: [CommonModule],
})
class Module {}
`);
const diags = env.driveDiagnostics();
expect(diags.length).toBe(1);
expect(diags[0].messageText)
.toContain(`is not assignable to type 'TrackByFunction<UnrelatedType>'.`);
});
it('should infer the context of NgFor', () => { it('should infer the context of NgFor', () => {
env.tsconfig({strictTemplates: true}); env.tsconfig({strictTemplates: true});
env.write('test.ts', ` env.write('test.ts', `

View File

@ -167,7 +167,7 @@ export interface TrackByFunction<T> {
* @param index The index of the item within the iterable. * @param index The index of the item within the iterable.
* @param item The item in the iterable. * @param item The item in the iterable.
*/ */
<U extends T>(index: number, item: U): any; <U extends T>(index: number, item: T&U): any;
} }
/** /**