Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(common): infer correct type when trackBy is used in ngFor #41995

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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> {
(index: number, item: T): any;
<U extends T>(index: number, item: U): any;
}

export declare const TRANSLATIONS: InjectionToken<string>;
Expand Down
19 changes: 12 additions & 7 deletions packages/compiler-cli/src/ngtsc/testing/fake_common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {NgIterable, TemplateRef, ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration, ɵɵPipeDeclaration} from '@angular/core';
import {NgIterable, TemplateRef, TrackByFunction, ɵɵDirectiveDeclaration, ɵɵNgModuleDeclaration, ɵɵPipeDeclaration} from '@angular/core';

export interface NgForOfContext<T, U extends NgIterable<T>> {
$implicit: T;
Expand All @@ -19,10 +19,6 @@ export interface NgForOfContext<T, U extends NgIterable<T>> {
index: number;
}

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

export interface NgIfContext<T = unknown> {
$implicit: T;
ngIf: T;
Expand Down Expand Up @@ -56,6 +52,7 @@ export declare class NgIf<T = unknown> {
'ngIfElse': 'ngIfElse';
}
, {}, never > ;
static ngTemplateGuard_ngIf: 'binding';
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any):
ctx is NgIfContext<Exclude<T, false|0|''|null|undefined>>;
}
Expand Down Expand Up @@ -83,8 +80,16 @@ export declare class DatePipe {
static ɵpipe: ɵɵPipeDeclaration<DatePipe, 'date'>;
}

export declare class IndexPipe {
transform<T>(value: T[], index: number): T;

static ɵpipe: ɵɵPipeDeclaration<IndexPipe, 'index'>;
}

export declare class CommonModule {
static ɵmod: ɵɵNgModuleDeclaration<
CommonModule, [typeof NgForOf, typeof NgIf, typeof DatePipe, typeof NgTemplateOutlet], never,
[typeof NgForOf, typeof NgIf, typeof DatePipe, typeof NgTemplateOutlet]>;
CommonModule,
[typeof NgForOf, typeof NgIf, typeof DatePipe, typeof IndexPipe, typeof NgTemplateOutlet],
never,
[typeof NgForOf, typeof NgIf, typeof DatePipe, typeof IndexPipe, typeof NgTemplateOutlet]>;
}
4 changes: 4 additions & 0 deletions packages/compiler-cli/src/ngtsc/testing/fake_core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,7 @@ export interface PipeTransform {
export interface OnDestroy {
ngOnDestroy(): void;
}

export interface TrackByFunction<T> {
<U extends T>(index: number, item: U): any;
}
1 change: 1 addition & 0 deletions packages/compiler-cli/test/ngtsc/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jasmine_node_test(
timeout = "long",
bootstrap = ["//tools/testing:node_no_angular_es5"],
data = [
"//packages/compiler-cli/src/ngtsc/testing/fake_common:npm_package",
"//packages/compiler-cli/src/ngtsc/testing/fake_core:npm_package",
],
shard_count = 4,
Expand Down
99 changes: 38 additions & 61 deletions packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {expectCompleteReuse, loadStandardTestFiles} from '../../src/ngtsc/testin

import {NgtscTestEnvironment} from './env';

const testFiles = loadStandardTestFiles();
const testFiles = loadStandardTestFiles({fakeCore: true, fakeCommon: true});

runInEachFileSystem(() => {
describe('ngtsc type checking', () => {
Expand All @@ -24,66 +24,6 @@ runInEachFileSystem(() => {
beforeEach(() => {
env = NgtscTestEnvironment.setup(testFiles);
env.tsconfig({fullTemplateTypeCheck: true});
env.write('node_modules/@angular/common/index.d.ts', `
import * as i0 from '@angular/core';

export declare class NgForOfContext<T, U extends i0.NgIterable<T> = i0.NgIterable<T>> {
$implicit: T;
count: number;
readonly even: boolean;
readonly first: boolean;
index: number;
readonly last: boolean;
ngForOf: U;
readonly odd: boolean;
constructor($implicit: T, ngForOf: U, index: number, count: number);
}

export declare class IndexPipe {
transform<T>(value: T[], index: number): T;

static ɵpipe: i0.ɵPipeDeclaration<IndexPipe, 'index'>;
}

export declare class SlicePipe {
transform<T>(value: ReadonlyArray<T>, start: number, end?: number): Array<T>;
transform(value: string, start: number, end?: number): string;
transform(value: null, start: number, end?: number): null;
transform(value: undefined, start: number, end?: number): undefined;
transform(value: any, start: number, end?: number): any;

static ɵpipe: i0.ɵPipeDeclaration<SlicePipe, 'slice'>;
}

export declare class NgForOf<T, U extends i0.NgIterable<T> = i0.NgIterable<T>> implements DoCheck {
ngForOf: (U & i0.NgIterable<T>) | undefined | null;
ngForTemplate: TemplateRef<NgForOfContext<T, U>>;
ngForTrackBy: TrackByFunction<T>;
constructor(_viewContainer: ViewContainerRef, _template: TemplateRef<NgForOfContext<T, U>>, _differs: IterableDiffers);
ngDoCheck(): void;
static ngTemplateContextGuard<T, U extends i0.NgIterable<T>>(dir: NgForOf<T, U>, ctx: any): ctx is NgForOfContext<T, U>;
static ɵdir: i0.ɵɵDirectiveDeclaration<NgForOf<any>, '[ngFor][ngForOf]', never, {'ngForOf': 'ngForOf'}, {}, never>;
}

export declare class NgIf<T = unknown> {
ngIf: T;
ngIfElse: TemplateRef<NgIfContext<T>> | null;
ngIfThen: TemplateRef<NgIfContext<T>> | null;
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<T>>);
static ngTemplateGuard_ngIf: 'binding';
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<Exclude<T, false | 0 | "" | null | undefined>>;
static ɵdir: i0.ɵɵDirectiveDeclaration<NgIf<any>, '[ngIf]', never, {'ngIf': 'ngIf'}, {}, never>;
}

export declare class NgIfContext<T = unknown> {
$implicit: T;
ngIf: T;
}

export declare class CommonModule {
static ɵmod: i0.ɵɵNgModuleDeclaration<CommonModule, [typeof NgIf, typeof NgForOf, typeof IndexPipe, typeof SlicePipe], never, [typeof NgIf, typeof NgForOf, typeof IndexPipe, typeof SlicePipe]>;
}
`);
env.write('node_modules/@angular/animations/index.d.ts', `
export declare class AnimationEvent {
element: any;
Expand Down Expand Up @@ -1000,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
Original file line number Diff line number Diff line change
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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment here that adds context to <U extends T> with a link to the issue?

}

/**
Expand Down