Skip to content

Commit

Permalink
fix(common): infer correct type when trackBy is used in ngFor (#4…
Browse files Browse the repository at this point in the history
…1995)

When a `trackBy` function is used that accepts a supertype of the iterated
array's type, the loop variable would undesirably be inferred as the supertype
instead of the array's item type. This commit adds an inferred type parameter
to `TrackByFunction` to allow an extra degree of freedom, enabling the
loop value to be inferred as the most narrow type.

Fixes #40125

PR Close #41995
  • Loading branch information
JoostK authored and thePunderWoman committed Jun 3, 2021
1 parent d69b91b commit 200cc31
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 3 deletions.
2 changes: 1 addition & 1 deletion goldens/public-api/core/core.d.ts
Expand Up @@ -952,7 +952,7 @@ export declare class TestabilityRegistry {
}

export declare interface TrackByFunction<T> {
(index: number, item: T): any;
<U extends T>(index: number, item: 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
Expand Up @@ -116,5 +116,5 @@ export interface OnDestroy {
}

export interface TrackByFunction<T> {
(index: number, item: T): any;
<U extends T>(index: number, item: U): any;
}
37 changes: 37 additions & 0 deletions packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
Expand Up @@ -940,6 +940,43 @@ export declare class AnimationEvent {
env.driveMain();
});

// https://github.com/angular/angular/issues/40125
it('should accept NgFor iteration when trackBy is used with a wider type', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
interface Base {
id: string;
}
interface Derived extends Base {
name: string;
}
@Component({
selector: 'test',
template: '<div *ngFor="let derived of derivedList; trackBy: trackByBase">{{derived.name}}</div>',
})
class TestCmp {
derivedList!: Derived[];
trackByBase(index: number, item: Base): string {
return item.id;
}
}
@NgModule({
declarations: [TestCmp],
imports: [CommonModule],
})
class Module {}
`);

env.driveMain();
});

it('should infer the context of NgFor', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
Expand Down
Expand Up @@ -157,11 +157,17 @@ export interface IterableChangeRecord<V> {
* @publicApi
*/
export interface TrackByFunction<T> {
// Note: the type parameter `U` enables more accurate template type checking in case a trackBy
// function is declared using a base type of the iterated type. The `U` type gives TypeScript
// additional freedom to infer a narrower type for the `item` parameter type, instead of imposing
// the trackBy's declared item type as the inferred type for `T`.
// See https://github.com/angular/angular/issues/40125

/**
* @param index The index of the item within the iterable.
* @param item The item in the iterable.
*/
(index: number, item: T): any;
<U extends T>(index: number, item: U): any;
}

/**
Expand Down

0 comments on commit 200cc31

Please sign in to comment.