docs: add docs about inputs and strictNullChecks to template typecheck (#34194)

PR Close #34194
This commit is contained in:
Kapunahele Wong 2019-11-26 17:21:49 -05:00 committed by Kara Erickson
parent af95dddd7e
commit a9e3cbd534
2 changed files with 167 additions and 91 deletions

View File

@ -691,94 +691,3 @@ In this example it is recommended to include the checking of `address` in the `*
}
}
```
### Input setter coercion
Occasionally it is desirable for the `@Input` of a directive or component to alter the value bound to it, typically using a getter/setter pair for the input. As an example, consider this custom button component:
Consider the following directive:
```typescript
@Component({
selector: 'submit-button',
template: `
<div class="wrapper">
<button [disabled]="disabled">Submit</button>'
</div>
`,
})
class SubmitButton {
private _disabled: boolean;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
}
}
```
Here, the `disabled` input of the component is being passed on to the `<button>` in the template. All of this works as expected, as long as a `boolean` value is bound to the input. But, suppose a consumer uses this input in the template as an attribute:
```html
<submit-button disabled></submit-button>
```
This has the same effect as the binding:
```html
<submit-button [disabled]="''"></submit-button>
```
At runtime, the input will be set to the empty string, which is not a `boolean` value. Angular component libraries that deal with this problem often "coerce" the value into the right type in the setter:
```typescript
set disabled(value: boolean) {
this._disabled = (value === '') || value;
}
```
It would be ideal to change the type of `value` here, from `boolean` to `boolean|''`, to match the set of values which are actually accepted by the setter. Unfortunately, TypeScript requires that both the getter and setter have the same type, so if the getter should return a `boolean` then the setter is stuck with the narrower type.
If the consumer has Angular's strictest type checking for templates enabled, this creates a problem: the empty string `''` is not actually assignable to the `disabled` field, which will create a type error when the attribute form is used.
As a workaround for this problem, Angular supports checking a wider, more permissive type for `@Input`s than is declared for the input field itself. This is enabled by adding a static property with the `ngAcceptInputType_` prefix to the component class:
```typescript
class SubmitButton {
private _disabled: boolean;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = (value === '') || value;
}
static ngAcceptInputType_disabled: boolean|'';
}
```
This field does not need to have a value. Its existence communicates to the Angular type checker that the `disabled` input should be considered as accepting bindings that match the type `boolean|''`. The suffix should be the `@Input` _field_ name.
Care should be taken that if an `ngAcceptInputType_` override is present for a given input, then the setter should be able to handle any values of the overridden type.
### Disabling type checking using `$any()`
Disable checking of a binding expression by surrounding the expression in a call to the [`$any()` cast pseudo-function](guide/template-syntax).
The compiler treats it as a cast to the `any` type just like in TypeScript when a `<any>` or `as any` cast is used.
In the following example, the error `Property addresss does not exist` is suppressed by casting `person` to the `any` type.
```typescript
@Component({
selector: 'my-component',
template: '{{$any(person).addresss.street}}'
})
class MyComponent {
person?: Person;
}
```

View File

@ -129,3 +129,170 @@ If that doesn't work, an option of last resort is to turn off full mode entirely
A type-checking error that you cannot resolve with any of the recommended methods can be the result of a bug in the template type-checker itself.
If you get errors that require falling back to basic mode, it is likely to be such a bug.
If this happens, please [file an issue](https://github.com/angular/angular/issues) so the team can address it.
## Inputs and type-checking
In Angular version 9, the template type checker checks whether a binding expression's type is compatible with that of the corresponding directive input.
As an example, consider the following component:
```typescript
export interface User {
name: string;
}
@Component({
selector: 'user-detail',
template: '{{ user.name }}',
})
export class UserDetailComponent {
@Input() user: User;
}
```
The `AppComponent` template uses this component as follows:
```ts
@Component({
selector: 'my-app',
template: '<user-detail [user]="selectedUser" />',
})
export class AppComponent {
selectedUser: User | null = null;
}
```
Here, during type checking of the template for `AppComponent`, the `[user]="selectedUser"` binding corresponds with the `UserDetailComponent.user` input.
Therefore, Angular assigns the `selectedUser` property to `UserDetailComponent.user`, which would result in an error if their types were incompatible.
TypeScript checks the assignment according to its type system, obeying flags such as `strictNullChecks` as they are configured in the application.
### Strict null checks
When you enable `strictTemplates` and the TypeScript flag `strictNullChecks`, typecheck errors may occur for certain situations that may not easily be avoided. For example:
* A nullable value that is bound to a directive from a library which did not have `strictNullChecks` enabled.
For a library compiled without `strictNullChecks`, its declaration files will not indicate whether a field can be `null` or not.
For situations where the library handles `null` correctly, this is problematic, as the compiler will check a nullable value against the declaration files which omit the `null` type.
As such, the compiler produces a type-check error because it adheres to `strictNullChecks`.
* Using the `async` pipe with an Observable which you know will emit synchronously.
The `async` pipe currently assumes that the Observable it subscribes to can be asynchronous, which means that it's possible that there is no value available yet.
In that case, it still has to return something&mdash;which is `null`.
In other words, the return type of the `async` pipe includes `null`, which may result in errors in situations where the Observable is known to emit a non-nullable value synchronously.
There are two potential workarounds to the above issues:
1. In the template, include the non-null assertion operator `!` at the end of a nullable expression, such as `<user-detail [user]="user!" />`.
In this example, the compiler disregards type incompatibilities in nullability, just as in TypeScript code.
In the case of the `async` pipe, note that the expression needs to be wrapped in parentheses, as in `<user-detail [user]="(user$ | async)!" />`.
1. Disable strict null checks in Angular templates completely.
When `strictTemplates` is enabled, it is still possible to disable certain aspects of type checking.
Setting the option `strictNullInputTypes` to `false` disables strict null checks within Angular templates.
This flag applies for all components that are part of the application.
### Advice for library authors
As a library author, you can take several measures to provide an optimal experience for your users.
First, enabling `strictNullChecks` and including `null` in an input's type, as appropriate, communicates to your consumers whether they can provide a nullable value or not.
Additionally, it is possible to provide type hints that are specific to the template type checker, see the [Input setter coercion](guide/template-typecheck#input-setter-coercion) section of this guide.
{@a input-setter-coercion}
## Input setter coercion
Occasionally it is desirable for the `@Input()` of a directive or component to alter the value bound to it, typically using a getter/setter pair for the input.
As an example, consider this custom button component:
Consider the following directive:
```typescript
@Component({
selector: 'submit-button',
template: `
<div class="wrapper">
<button [disabled]="disabled">Submit</button>'
</div>
`,
})
class SubmitButton {
private _disabled: boolean;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = value;
}
}
```
Here, the `disabled` input of the component is being passed on to the `<button>` in the template. All of this works as expected, as long as a `boolean` value is bound to the input. But, suppose a consumer uses this input in the template as an attribute:
```html
<submit-button disabled></submit-button>
```
This has the same effect as the binding:
```html
<submit-button [disabled]="''"></submit-button>
```
At runtime, the input will be set to the empty string, which is not a `boolean` value. Angular component libraries that deal with this problem often "coerce" the value into the right type in the setter:
```typescript
set disabled(value: boolean) {
this._disabled = (value === '') || value;
}
```
It would be ideal to change the type of `value` here, from `boolean` to `boolean|''`, to match the set of values which are actually accepted by the setter.
TypeScript requires that both the getter and setter have the same type, so if the getter should return a `boolean` then the setter is stuck with the narrower type.
If the consumer has Angular's strictest type checking for templates enabled, this creates a problem: the empty string `''` is not actually assignable to the `disabled` field, which will create a type error when the attribute form is used.
As a workaround for this problem, Angular supports checking a wider, more permissive type for `@Input()` than is declared for the input field itself. Enable this by adding a static property with the `ngAcceptInputType_` prefix to the component class:
```typescript
class SubmitButton {
private _disabled: boolean;
get disabled(): boolean {
return this._disabled;
}
set disabled(value: boolean) {
this._disabled = (value === '') || value;
}
static ngAcceptInputType_disabled: boolean|'';
}
```
This field does not need to have a value. Its existence communicates to the Angular type checker that the `disabled` input should be considered as accepting bindings that match the type `boolean|''`. The suffix should be the `@Input` _field_ name.
Care should be taken that if an `ngAcceptInputType_` override is present for a given input, then the setter should be able to handle any values of the overridden type.
## Disabling type checking using `$any()`
Disable checking of a binding expression by surrounding the expression in a call to the [`$any()` cast pseudo-function](guide/template-syntax).
The compiler treats it as a cast to the `any` type just like in TypeScript when a `<any>` or `as any` cast is used.
In the following example, casting `person` to the `any` type suppresses the error `Property address does not exist`.
```typescript
@Component({
selector: 'my-component',
template: '{{$any(person).addresss.street}}'
})
class MyComponent {
person?: Person;
}
```