feat(ivy): input type coercion for template type-checking (#33243)
Often the types of an `@Input`'s field don't fully reflect the types of assignable values. This can happen when an input has a getter/setter pair where the getter always returns a narrow type, and the setter coerces a wider value down to the narrow type. For example, you could imagine an input of the form: ```typescript @Input() get value(): string { return this._value; } set value(v: {toString(): string}) { this._value = v.toString(); } ``` Here, the getter always returns a `string`, but the setter accepts any value that can be `toString()`'d, and coerces it to a string. Unfortunately TypeScript does not actually support this syntax, and so Angular users are forced to type their setters as narrowly as the getters, even though at runtime the coercion works just fine. To support these kinds of patterns (e.g. as used by Material), this commit adds a compiler feature called "input coercion". When a binding is made to the 'value' input of a directive like MatInput, the compiler will look for a static field with the name ngAcceptInputType_value. If such a field is found the type-checking expression for the input will use the static field's type instead of the type for the @Input field,allowing for the expression of a type conversion between the binding expression and the value being written to the input's field. To solve the case above, for example, MatInput might write: ```typescript class MatInput { // rest of the directive... static ngAcceptInputType_value: {toString(): string}; } ``` FW-1475 #resolve PR Close #33243
This commit is contained in:

committed by
Andrew Kushnir

parent
a1d7b6bb86
commit
f1269d98dc
@ -406,6 +406,76 @@ export declare class CommonModule {
|
||||
expect(diags[1].length).toEqual(15);
|
||||
});
|
||||
|
||||
describe('input coercion', () => {
|
||||
beforeEach(() => {
|
||||
env.tsconfig({
|
||||
'fullTemplateTypeCheck': true,
|
||||
});
|
||||
env.write('node_modules/@angular/material/index.d.ts', `
|
||||
import * as i0 from '@angular/core';
|
||||
|
||||
export declare class MatInput {
|
||||
value: string;
|
||||
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatInput, '[matInput]', never, {'value': 'value'}, {}, never>;
|
||||
static ngAcceptInputType_value: string|number;
|
||||
}
|
||||
|
||||
export declare class MatInputModule {
|
||||
static ɵmod: i0.ɵɵNgModuleDefWithMeta<MatInputModule, [typeof MatInput], never, [typeof MatInput]>;
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should coerce an input using a coercion function if provided', () => {
|
||||
env.write('test.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
import {MatInputModule} from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'blah',
|
||||
template: '<input matInput [value]="someNumber">',
|
||||
})
|
||||
export class FooCmp {
|
||||
someNumber = 3;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FooCmp],
|
||||
imports: [MatInputModule],
|
||||
})
|
||||
export class FooModule {}
|
||||
`);
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should give an error if the binding expression type is not accepted by the coercion function',
|
||||
() => {
|
||||
env.write('test.ts', `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
import {MatInputModule} from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 'blah',
|
||||
template: '<input matInput [value]="invalidType">',
|
||||
})
|
||||
export class FooCmp {
|
||||
invalidType = true;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [FooCmp],
|
||||
imports: [MatInputModule],
|
||||
})
|
||||
export class FooModule {}
|
||||
`);
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(diags[0].messageText)
|
||||
.toBe(`Type 'boolean' is not assignable to type 'string | number'.`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy schema checking with the DOM schema', () => {
|
||||
beforeEach(
|
||||
() => { env.tsconfig({ivyTemplateTypeCheck: true, fullTemplateTypeCheck: false}); });
|
||||
|
Reference in New Issue
Block a user