Skip to content

Commit

Permalink
fix(core): allow proper type inference when ngFor is used with a `t…
Browse files Browse the repository at this point in the history
…rackBy` function

In angular#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 angular#42609
  • Loading branch information
JoostK committed Jun 28, 2021
1 parent 41823ff commit 30c3b92
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 3 deletions.
2 changes: 1 addition & 1 deletion goldens/public-api/core/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,7 @@ export declare class TestabilityRegistry {
}

export declare interface TrackByFunction<T> {
<U extends T>(index: number, item: U): any;
<U extends T>(index: number, item: T & U): any;
}

export declare const TRANSLATIONS: InjectionToken<string>;
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/src/ngtsc/testing/fake_core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,5 @@ export interface OnDestroy {
}

export interface TrackByFunction<T> {
<U extends T>(index: number, item: U): any;
<U extends T>(index: number, item: T&U): any;
}
72 changes: 72 additions & 0 deletions packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,78 @@ export declare class AnimationEvent {
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', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export interface TrackByFunction<T> {
* @param index The index of the item within 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;
}

/**
Expand Down

0 comments on commit 30c3b92

Please sign in to comment.