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

Ivy compiler incorrectly allows undefined to be passed to any component input even without explicit undefined in input type when strictNullChecks is on #32690

Closed
compeek opened this issue Sep 16, 2019 · 7 comments
Labels
area: compiler Issues related to `ngc`, Angular's template compiler
Milestone

Comments

@compeek
Copy link

compeek commented Sep 16, 2019

🐞 bug report

Affected Package

Not exactly sure, but probably:
@angular/compiler
@angular/compiler-cli

Is this a regression?

No, from my experience, the old compiler didn't enforce strictNullChecks in templates at all, so this is new to Ivy.

Description

With strictNullChecks turned on, if you have a component input with a certain type that does not explicitly include undefined, the Ivy compiler does not complain if you pass undefined to that input from another component. This defeats a lot of the purpose of using strictNullChecks. (Note that it does complain about passing null if null isn't explicitly in the type, so I know strictNullChecks is enabled properly.)

I believe this is due to the use of Partial<Pick<...>> in the type check block for the template as described here: https://blog.angularindepth.com/type-checking-templates-in-angular-viewengine-and-ivy-77f8536359f5

Partial<...> causes each property to be made optional, which in effect adds undefined to the type of each property as well, as described in the TypeScript docs: Optional parameters and properties automatically have undefined added to their types, even when their type annotations don’t specifically include undefined.. (https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#optional-parameters-and-properties)

So, what seems to be happening is the Ivy compiler is adding undefined to the type of every input when considering the bindings, which means it will never complain about passing undefined to the input of any component. That really makes strictNullChecks a lot less useful.

Please understand that although nobody in practice would probably pass a hardcoded undefined to a component input, this is an actual problem any time you use undefined to represent the absence of a value being passed to a component. In my case, I have a store I subscribe to using the async pipe, and then some of the state gets passed into child components from there. Various properties on my state have types like SomeModel | undefined. undefined could mean the model is not yet loaded, for example. Although some people may use null instead, there seems to be a significant group that prefers to use undefined in all cases. Wherever you fall on that debate, my point is that this definitely comes into play in real projects, and in fact I'm having the issue after switching to Ivy in my own project. I can't have a lot of confidence in what I'm passing to my components' inputs if undefined is always allowed by the compiler regardless of the actual type of the inputs.

🔬 Minimal Reproduction

https://github.com/compeek/angular-ivy-input-undefined-issue

If you compile this project, you should only see a compiler error about null and not about undefined as well. See index.html for some more explanation of the example.

You can reproduce this easily in any project with strictNullChecks turned on and the Ivy compiler enabled with these two components (from my minimal reproduction project):

@Component({
  selector: 'app-root',
  template: `<app-child [stringOnlyInput1]="thisPropertyIsUndefined" [stringOnlyInput2]="thisPropertyIsNull"></app-child>`
})
export class AppComponent {
  thisPropertyIsUndefined: undefined = undefined;
  thisPropertyIsNull: null = null;
}

@Component({
  selector: 'app-child',
  template: `
    {{ 'stringOnlyInput1: ' + stringOnlyInput1 }}
    <br>
    {{ 'stringOnlyInput2: ' + stringOnlyInput2 }}
  `
})
export class ChildComponent {
  @Input() stringOnlyInput1: string = '';
  @Input() stringOnlyInput2: string = '';
}

The compiler should complain about the values passed to both inputs, but in fact it only complains about the null value and not undefined.

🔥 Exception or Error

I would expect an error like:
Type 'undefined' is not assignable to type 'string'.

But none is thrown.

And in fact if you do it with null, the error looks like this:
Type 'null' is not assignable to type 'string | undefined'.

Notice how it says it's not assignable to string | undefined even though the input type is just string.

🌍 Your Environment

Angular Version:



Angular CLI: 9.0.0-next.4
Node: 10.16.3
OS: win32 x64
Angular: 9.0.0-next.6
... animations, common, compiler, compiler-cli, core
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.900.0-next.4
@angular-devkit/build-angular     0.900.0-next.4
@angular-devkit/build-optimizer   0.900.0-next.4
@angular-devkit/build-webpack     0.900.0-next.4
@angular-devkit/core              9.0.0-next.4
@angular-devkit/schematics        9.0.0-next.4
@angular/cli                      9.0.0-next.4
@ngtools/webpack                  9.0.0-next.4
@schematics/angular               9.0.0-next.4
@schematics/update                0.900.0-next.4
rxjs                              6.5.3
typescript                        3.5.3
webpack                           4.39.3

Anything else relevant?

@compeek compeek changed the title Ivy compiler incorrectly allows undefined values for all inputs even without explicit undefined in input type when strictNullChecks is on Ivy compiler incorrectly allows undefined values pass to any input even without explicit undefined in input type when strictNullChecks is on Sep 16, 2019
@compeek compeek changed the title Ivy compiler incorrectly allows undefined values pass to any input even without explicit undefined in input type when strictNullChecks is on Ivy compiler incorrectly allows undefined to be passed to any input even without explicit undefined in input type when strictNullChecks is on Sep 16, 2019
@compeek compeek changed the title Ivy compiler incorrectly allows undefined to be passed to any input even without explicit undefined in input type when strictNullChecks is on Ivy compiler incorrectly allows undefined to be passed to any component input even without explicit undefined in input type when strictNullChecks is on Sep 16, 2019
@AndrewKushnir AndrewKushnir added area: compiler Issues related to `ngc`, Angular's template compiler comp: ivy labels Sep 16, 2019
@ngbot ngbot bot added this to the needsTriage milestone Sep 16, 2019
@alxhub
Copy link
Member

alxhub commented Oct 8, 2019

This is https://angular-team.atlassian.net/browse/FW-1606. @JoostK has a fix :)

@compeek
Copy link
Author

compeek commented Oct 9, 2019

This is https://angular-team.atlassian.net/browse/FW-1606. Joost Koehoorn has a fix :)

Unfortunately I do not have access to that, so I'm not sure what you're referencing. :( However if there's a fix, that's great! Any idea when it will be shipped?

@compeek compeek closed this as completed Oct 9, 2019
@compeek compeek reopened this Oct 9, 2019
@JoostK
Copy link
Member

JoostK commented Oct 9, 2019

@compeek the idea is that we will remove the Partial mapped type and instead ensure to generate all possible inputs in the object literal, where any input that is not used is set to null as any. This achieves the same goal of allowing not all inputs to be present, without undesirably introducing undefined for all inputs.

Expect a PR to be out later this week.

@compeek
Copy link
Author

compeek commented Oct 9, 2019

@JoostK, when you say any input that is not used is set to null as any, do you mean that any input not specified in the template will automatically get a value of null passed to it? If so, wouldn't that override any default values set on those input properties?

@JoostK
Copy link
Member

JoostK commented Oct 9, 2019

Oh these changes will all be in the generated code that's used for type checking, which will never actually execute. The generated code that the compiler generates for runtime execution will not be affected.

@compeek
Copy link
Author

compeek commented Oct 9, 2019

Oh, I see now. Excellent. Thank you for the information! I look forward to this fix coming out in a "next" release so I can start using it in my project.

JoostK added a commit to JoostK/angular that referenced this issue Oct 9, 2019
Prior to this change, the template type checker would always allow a
value of type `undefined` to be passed into a directive's inputs, even
if the input's type did not allow for it. This was due to how the type
constructor for a directive was generated, where a `Partial` mapped
type was used to allow for inputs to be unset. This essentially
introduces the `undefined` type as acceptable type for all inputs.

This commit removes the `Partial` type from the type constructor, which
means that we can no longer omit any properties that were unset.
Instead, any properties that are not set will still be included in the
type constructor call, having their value assigned to `any`.

Before:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']});
```

After:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {
  ngForOf: ['foo', 'bar'],
  ngForTrackBy: null as any,
  ngForTemplate: null as any,
});
```

This change only affects generated type check code, the generated
runtime code is not affected.

Fixes angular#32690
Resolves FW-1606
JoostK added a commit to JoostK/angular that referenced this issue Oct 10, 2019
Prior to this change, the template type checker would always allow a
value of type `undefined` to be passed into a directive's inputs, even
if the input's type did not allow for it. This was due to how the type
constructor for a directive was generated, where a `Partial` mapped
type was used to allow for inputs to be unset. This essentially
introduces the `undefined` type as acceptable type for all inputs.

This commit removes the `Partial` type from the type constructor, which
means that we can no longer omit any properties that were unset.
Instead, any properties that are not set will still be included in the
type constructor call, having their value assigned to `any`.

Before:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']});
```

After:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {
  ngForOf: ['foo', 'bar'],
  ngForTrackBy: null as any,
  ngForTemplate: null as any,
});
```

This change only affects generated type check code, the generated
runtime code is not affected.

Fixes angular#32690
Resolves FW-1606
JoostK added a commit to JoostK/angular that referenced this issue Oct 11, 2019
Prior to this change, the template type checker would always allow a
value of type `undefined` to be passed into a directive's inputs, even
if the input's type did not allow for it. This was due to how the type
constructor for a directive was generated, where a `Partial` mapped
type was used to allow for inputs to be unset. This essentially
introduces the `undefined` type as acceptable type for all inputs.

This commit removes the `Partial` type from the type constructor, which
means that we can no longer omit any properties that were unset.
Instead, any properties that are not set will still be included in the
type constructor call, having their value assigned to `any`.

Before:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']});
```

After:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {
  ngForOf: ['foo', 'bar'],
  ngForTrackBy: null as any,
  ngForTemplate: null as any,
});
```

This change only affects generated type check code, the generated
runtime code is not affected.

Fixes angular#32690
Resolves FW-1606
JoostK added a commit to JoostK/angular that referenced this issue Oct 12, 2019
Prior to this change, the template type checker would always allow a
value of type `undefined` to be passed into a directive's inputs, even
if the input's type did not allow for it. This was due to how the type
constructor for a directive was generated, where a `Partial` mapped
type was used to allow for inputs to be unset. This essentially
introduces the `undefined` type as acceptable type for all inputs.

This commit removes the `Partial` type from the type constructor, which
means that we can no longer omit any properties that were unset.
Instead, any properties that are not set will still be included in the
type constructor call, having their value assigned to `any`.

Before:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']});
```

After:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {
  ngForOf: ['foo', 'bar'],
  ngForTrackBy: null as any,
  ngForTemplate: null as any,
});
```

This change only affects generated type check code, the generated
runtime code is not affected.

Fixes angular#32690
Resolves FW-1606
ODAVING pushed a commit to ODAVING/angular that referenced this issue Oct 18, 2019
…lar#33066)

Prior to this change, the template type checker would always allow a
value of type `undefined` to be passed into a directive's inputs, even
if the input's type did not allow for it. This was due to how the type
constructor for a directive was generated, where a `Partial` mapped
type was used to allow for inputs to be unset. This essentially
introduces the `undefined` type as acceptable type for all inputs.

This commit removes the `Partial` type from the type constructor, which
means that we can no longer omit any properties that were unset.
Instead, any properties that are not set will still be included in the
type constructor call, having their value assigned to `any`.

Before:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']});
```

After:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {
  ngForOf: ['foo', 'bar'],
  ngForTrackBy: null as any,
  ngForTemplate: null as any,
});
```

This change only affects generated type check code, the generated
runtime code is not affected.

Fixes angular#32690
Resolves FW-1606

PR Close angular#33066
AndrusGerman pushed a commit to AndrusGerman/angular that referenced this issue Oct 22, 2019
…lar#33066)

Prior to this change, the template type checker would always allow a
value of type `undefined` to be passed into a directive's inputs, even
if the input's type did not allow for it. This was due to how the type
constructor for a directive was generated, where a `Partial` mapped
type was used to allow for inputs to be unset. This essentially
introduces the `undefined` type as acceptable type for all inputs.

This commit removes the `Partial` type from the type constructor, which
means that we can no longer omit any properties that were unset.
Instead, any properties that are not set will still be included in the
type constructor call, having their value assigned to `any`.

Before:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Partial<Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {ngForOf: ['foo', 'bar']});
```

After:

```typescript
class NgForOf<T> {
  static ngTypeCtor<T>(init: Pick<NgForOf<T>,
    'ngForOf'|'ngForTrackBy'|'ngForTemplate'>): NgForOf<T>;
}

NgForOf.ngTypeCtor(init: {
  ngForOf: ['foo', 'bar'],
  ngForTrackBy: null as any,
  ngForTemplate: null as any,
});
```

This change only affects generated type check code, the generated
runtime code is not affected.

Fixes angular#32690
Resolves FW-1606

PR Close angular#33066
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Nov 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: compiler Issues related to `ngc`, Angular's template compiler
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants