angular icon indicating copy to clipboard operation
angular copied to clipboard

Canonical: Marking @Input as required

Open thw0rted opened this issue 5 years ago • 30 comments

🚀 feature request

Relevant Package

This feature request is for @angular/core

Description

There is a long history of discussion about whether / how to have the compiler enforce that specific inputs are provided to components: #11904 #18156 #24879 #28403 #30974

However, all of those are now closed and locked. They contain some advice about how to mitigate the problem, or kick the can down the road ("One day I hope we'll be able to do this. That day is not today."), but thanks to the lock-bot none of them can be used to continue the conversation, so I guess we need yet another issue, because I believe the problem still exists.

Describe the solution you'd like

It should be possible to annotate inputs such that using a component in a template without providing a value for that input is a compile-time error. Template checking has come a long way, and it's already possible for the compiler to flag providing the wrong type as an argument. Combine with the strictPropertyInitializers flag, and such a check could rely on the existing ! assertion operator.

Describe alternatives you've considered

The best approach I've seen while reviewing the above issues is specifying the required attributes in the selector component metadata, but a) apparently that won't catch passing null or undefined to an Input that not defined with a type that includes those values, and b) that appears to break certain test harnesses? Also, I haven't started using strictTemplates / fullTemplateTypeCheck yet due to some problems in previous versions with narrowing on *ngIf that may or may not have been resolved -- those existing capabilities may address some or all of this need.

thw0rted avatar Jun 24 '20 16:06 thw0rted

Combine with the strictPropertyInitializers flag, and such a check could rely on the existing ! assertion operator.

For information, it is what Ionic's Stencil is doing.

cyrilletuzi avatar Jun 25 '20 16:06 cyrilletuzi

apparently that won't catch passing null or undefined

This is already handled by strictTemplates angular compiler setting (in particular, strictNullInputTypes)

I'm wondering though if adding the input names to the directive selector is actually the official recommendation to achieve the goal - or rather a hackish workaround. If it is recommended - maybe it should be fixed as such in the docs?

amakhrov avatar Oct 28 '20 20:10 amakhrov

I'm wondering though if adding the input names to the directive selector is actually the official recommendation to achieve the goal - or rather a hackish workaround. If it is recommended - maybe it should be fixed as such in the docs?

Even if you fix the docs, and start promoting this approach, I still think it's not ok. First, the error will be wrong, as the compiler will complain about not finding the component, instead of the desired error. Secondly, if you leave the selector, without the attribute, and use the component. You will end up in with a strange behavior. If you pass an undefined value, the compiler will complain, but if you skip the attribute, it will work, even though the runtime behavior is the same.

That's why I think, the best approach is to treat them the same, meaning that an error should occur, similar to passing an undefined value, as that's how the runtime behavior is.

Edit: The above example is reproducible in strict mode, I'm not sure if it works outside it.

ratoi-crysty avatar Jan 11 '21 23:01 ratoi-crysty

Any news on that?

amakhrov avatar May 12 '22 00:05 amakhrov

apparently that won't catch passing null or undefined

This is already handled by strictTemplates angular compiler setting (in particular, strictNullInputTypes)

I'm wondering though if adding the input names to the directive selector is actually the official recommendation to achieve the goal - or rather a hackish workaround. If it is recommended - maybe it should be fixed as such in the docs?

I think I have a working solution https://github.com/angular/angular/issues/46949 - but my solution is with setters and it just got marked as a duplicate - since I'm not so familiar with the eco system of those things could someone please check my proposal I even linked the solution.

There is an alternative way to make simple inputs work by checking them onChanges manually.

muhamedkarajic avatar Jul 24 '22 00:07 muhamedkarajic

The solution proposed in https://github.com/angular/angular/issues/46949 is a run-time check, which is already possible with custom code in ngOnInit. It just abstracts away some repeating boilerplate.

What we need though is a compile-time check. I sort of realize why it's hard to do given the existing angular components syntax and TS capabilities. Yet I don't lose hope that someone smarter than me comes up with a dev-friendly solution for it. A lack of such an approach makes null safety in an Angular app impossible. This fails to satisfy expectations (or I daresay, requirements) to modern code.

amakhrov avatar Jul 24 '22 05:07 amakhrov

The solution proposed in #46949 is a run-time check, which is already possible with custom code in ngOnInit. It just abstracts away some repeating boilerplate.

It gets the job done and well, what do you want from a framework to write for every input every time check me onInit A,B and C? Also this isn't right at all it dosent work onInit (need to refactor the function names). It changes the value while making the metadata of the class and it overrides (in my current example) the setter so it functions like a guard - every time you change the input it checks the function which it overrided and then triggers the original function. There is a solution without setters by using onChanges - it would override the current onChanges where it would do the checks and then it would trigger the original onChanges. I can code it but I wanted to see how the feedback will be on this and its the worst case scenario.

What we need though is a compile-time check. I sort of realize why it's hard to do given the existing angular components syntax and TS capabilities.

Okay but this is pretty much impossible in .NET so why do you expect it to be possible in TypeScript / JavaScript? Imagine I get a library from someone else, the return type is number but at some point I get null / undefined or NaN. This is JavaScript and its duct typing - so why do we set such high standards? What is really the reason you don't accept my solution if its working and will work in 100% cases? Instead you make the point that you hope and think someone smarter will come - more on that below.

Yet I don't lose hope that someone smarter than me comes up with a dev-friendly solution for it. A lack of such an approach makes null safety in an Angular app impossible. This fails to satisfy expectations (or I daresay, requirements) to modern code.

This is the most disappointing part of your comment and when I read it it made me totally confused. I have so many thoughts on this but I'll just be streight and say that my solution is a working solution which I tested and I could not find anywhere such a solution.

My arguments:

  • A working solution today is far better then maybe a hope tomorrow.
  • This solution will work in 100% cases I don't know what hope you have for a typesafe solution but it won't work always.
  • I can't believe I have to say this but having per input 10 lines of code less is a good thing.
  • This would help to avoid a lot of bugs, everytime you get something which you don't expect you will see it as an error. As it should be from my point of view - we do the same in backend languages in every constructor we check if the value exists and if not we throw an error.
  • This has been open for 2 years for such a simple feature do you want everyone now to wait 2 more years :)
  • It tells you for which NgComponent and Input the error happened.
  • Its probably possible to extend the current @Input and @Component decorator to work with this without breaking changes.
  • My solution is also not a breaking change and its declerative per individual Input.

I kind of see where you are guys comming from but I think together we will be that 'smarter person' so my solution dose do a compile time change and then it executes the specific function for that specific input every time the input changes.

Also I'm open for discussions but you need to point out objective arguments I can't accept an argument to be 'hope'.

muhamedkarajic avatar Jul 25 '22 01:07 muhamedkarajic

In .net you have constructors where you must specify all the parameters.

Btw it the same here - basically this fields must be treated as required constructor fields for control, represented by particular template usage

Lonli-Lokli avatar Jul 25 '22 07:07 Lonli-Lokli

Hi @muhamedkarajic , I like the idea of extending @Input as a way of marking-as-required, but I think you may be confused about the intent of this issue (and the previous ones that I linked in my original write-up). Angular uses TypeScript, and the whole point of using TypeScript instead of vanilla JavaScript is to get compile-time, rather than run-time, error handling. Having to build and run your project before seeing the error is simply a worse developer experience. Your code is already capable of capturing whether a value can possibly be null or undefined, let's take advantage of that!

As I wrote previously, it's already possible to generate a compile-time error when a required input is omitted, it just happens to be a somewhat confusing error. When a previous comment referred to a "dev friendly" solution, I believe they meant that the ideal feature would generate an error message immediately in your IDE, that tells you exactly what you did wrong (i.e., omitted a required input).

thw0rted avatar Jul 25 '22 07:07 thw0rted

Hi @muhamedkarajic , I like the idea of extending @Input as a way of marking-as-required, but I think you may be confused about the intent of this issue (and the previous ones that I linked in my original write-up).

My proposal was just linked as duplicate and now here I'm.

Angular uses TypeScript, and the whole point of using TypeScript instead of vanilla JavaScript is to get compile-time, rather than run-time, error handling. Having to build and run your project before seeing the error is simply a worse developer experience.

I understood that and I gave good examples why thats not going to work for a lot of cases.

Your code is already capable of capturing whether a value can possibly be null or undefined, let's take advantage of that!

Thank you but I don't know what to do from here on, unless someone explains it to me.

As I wrote previously, it's already possible to generate a compile-time error when a required input is omitted, it just happens to be a somewhat confusing error. When a previous comment referred to a "dev friendly" solution, I believe they meant that the ideal feature would generate an error message immediately in your IDE, that tells you exactly what you did wrong (i.e., omitted a required input).

I didn't catch the hole conversation cause I just got here because my proposal was marked as duplicate. Still I just don't see how that is supposed to be possible, my solution is more to make your life easier to catch bugs when you have edge cases, regarding the required input .NET has that as well but you still have the issue that the value can be null - the typescript compiler just can't fetch such a thing cause a type is one thing and a true compile time value is anothing thing.

muhamedkarajic avatar Jul 25 '22 09:07 muhamedkarajic

In .net you have constructors where you must specify all the parameters.

Btw it the same here - basically this fields must be treated as required constructor fields for control, represented by particular template usage

Can't you do that already with '!' I think someone wrote that but thats still having that issue that in compile time something which is null can be still passed as input - same issue with .NET even if you expect an object your library can sent null / undefined and you then get an unexpected error or even wrost things seem to go fine but arent.

muhamedkarajic avatar Jul 25 '22 09:07 muhamedkarajic

It looks like you maybe haven't spent a lot of time with Typescript, which might be the source of some of your confusion. In TS, null and undefined are their own types, and a property or variable can have a type of e.g. number without allowing null or undefined. In that case, any code that might assign those values will be flagged as an error at compile-time. That's what I meant when I said

Your code is already capable of capturing whether a value can possibly be null or undefined

If your code is properly typed, and does not work outside the type system (casting to any, using @ts-ignore, etc), you can be confident that the runtime value will definitely never be null or undefined. That's the whole point of the strictNullChecks flag.

The reason for this issue is that this safety can be extended to the template checker, to ensure that you never bind components in a way that will pass null or undefined to an input that expects never to get those values. We just want the template engine to properly preserve the (compile-time) safety guarantees that the type-checker already provides.

thw0rted avatar Jul 25 '22 10:07 thw0rted

It looks like you maybe haven't spent a lot of time with Typescript, which might be the source of some of your confusion. In TS, null and undefined are their own types, and a property or variable can have a type of e.g. number without allowing null or undefined. In that case, any code that might assign those values will be flagged as an error at compile-time.

This is totally known to any developer who uses type saftly but it dosent work for third party APIs when they send something which you don't expect - sure you can define a interface which has a number which can't be null but you can't ensure it will be null at that moment which you are expecting (in the run time).

If your code is properly typed, and does not work outside the type system (casting to any, using @ts-ignore, etc), you can be confident that the runtime value will definitely never be null or undefined. That's the whole point of the strictNullChecks flag.

If we are talking about 'required' inputs I don't expect it to be typesafe only I expect it to be something which is required and the component won't work without providing it. Your solution would still mean the current Angular behavior is present and thats rendering the component even if the input is null - again those edge cases which you can't make typesafe.

For me required means runtime errors if the value isnt of the expected type.

What else would be possible with the runtime solution would be that you can define configuration per project what should happen if the result isnt the one which is expected, e.g. error, warrning, ignore setting the new input value. For the type-safty I just don't see the overall benefit for something more serious, the current Angular behavior seems fine to me, maybe not the best errors but that wasn't the reason I proposed the solution in the first place - my proposal was not regarding this one it was just marked as duplicated cause it refers to the same topic.

muhamedkarajic avatar Jul 27 '22 21:07 muhamedkarajic

I see, you clearly want runtime checks, and you are correct that this issue focuses on the type checker instead. I would be surprised if the Angular team wants to implement runtime checks like you're suggesting - they typically have a performance cost - but you are welcome to ask them to reopen your issue.

thw0rted avatar Jul 27 '22 22:07 thw0rted

I see, you clearly want runtime checks, and you are correct that this issue focuses on the type checker instead. I would be surprised if the Angular team wants to implement runtime checks like you're suggesting - they typically have a performance cost - but you are welcome to ask them to reopen your issue.

I have written in my proposal to reopen it they said they will take a look at every option - therefor I'm here. The performance issue isn't in my oppinion a cost cause you don't need to implement that decorator if you don't want to and if you do implement it - it just means you saved the boilerplate, it would function like a guard you have guards for routing etc why not having them for inputs.

muhamedkarajic avatar Jul 27 '22 22:07 muhamedkarajic

I believe the runtime check is just a nice way to do what's already possible in ngOnInit or ngOnChanges with the ! operator. I believe having the ability to get an error on the component template declaration stating that input X is missing but required is a great benefit. The whole type system is meant for those checks in compile time vs runtime to catch bugs as early as possible. Concerning the actual framework implementation - I am not sure the ! operator can be used to type check but that's of course the best solution. In case it's not possible - maybe a compile time known decorator like input will do the trick. Only thing worth thinking about is missing vs undefined which I believe should be based on exactOptionalPropertyTypes - or if not possible a parameter to the mentioned required decorator.

Harpush avatar Aug 03 '22 06:08 Harpush

...having the ability to get an error on the component template declaration stating that input X is missing but required is a great benefit.

^^

Only thing worth thinking about is missing vs undefined which I believe should be based on exactOptionalPropertyTypes - or if not possible a parameter to the mentioned required decorator.

Regarding the undefined vs missing that can be done by having a boolean which will be set to true if the setter is ever called (before onInit), then OnInit you can check all inputs were set.

Could anyone help me find the source code of the Angular Input Decorator? I found this link but I don't see the @Input decorator.

muhamedkarajic avatar Aug 03 '22 20:08 muhamedkarajic

OK so again, this issue is not about runtime checks, it is about changing the behavior of the template engine. If you'd like to continue discussing runtime checks please do so in the issue you opened about those, to avoid confusing future visitors here. (The runtime-check conversation already represents more than half the posts in this thread!)

thw0rted avatar Aug 04 '22 07:08 thw0rted

OK so again, this issue is not about runtime checks, it is about changing the behavior of the template engine. If you'd like to continue discussing runtime checks please do so in the issue you opened about those, to avoid confusing future visitors here. (The runtime-check conversation already represents more than half the posts in this thread!)

You can feel free to write in the closed issue your point of view: https://github.com/angular/angular/issues/46949.

I made my point clear and the message from the person who marked my post as duplicate was also clear. Dosen't also help that this is 2 years old and nothing moved.

I don't mind if no one accepts my solution but thats on the angular dev team to decide so please stop forcing me to open a new issue.

muhamedkarajic avatar Aug 09 '22 22:08 muhamedkarajic

You can feel free to write in the closed issue your point of view

Done. (I didn't want you to open a new issue, I wanted to argue to reopen your closed issue, so I argued for that.)

Dosen't also help that this is 2 years old and nothing moved.

Actually, very nearly 6 years -- due to the lock-bot, this issue keeps getting opened, locked, then opened again. There's a list in the OP.

thw0rted avatar Aug 10 '22 06:08 thw0rted

Done. (I didn't want you to open a new issue, I wanted to argue to reopen your closed issue, so I argued for that.)

I have tried that and as the other person said they are going to consider all possibilities.

muhamedkarajic avatar Aug 10 '22 10:08 muhamedkarajic

What about @RequireBinding() decorator that would only enforce that the binding in template occurs, it can be bind to undefined, null, a function or a field or primitive - basicly whatever. All that it would be responsible would enforce that

@RequireBinding()
@Input() a: string | undefined | null;

<component-with-required-field-a></component-with-required-field-a> wold throw compile time error <component-with-required-field-a [a]="undefined"></component-with-required-field-a> wold not

I propose @RequireBinding instead of @Required as it clearly states that it is binding that is required cause without it mayby component doesn't make sense - for example form component with [formControl] input.

Alternative would be adding a flag to @Input(name, {requireBinding: true}) or @Input({requireBinding: true})

It could probably even be used with outputs as well

@RequireBinding()
@Output() clicked = new EventEmmiter();

witch would indicate that we are expecting consumer to handle that event.

RequireBinding of course would not stop users from using compoennts dynamicly without providing any inputs as it would be strictly developer friendly addition for templating only.

kbrilla avatar Sep 06 '22 08:09 kbrilla

What about @RequireBinding() decorator that would only enforce that the binding in template occurs, it can be bind to undefined, null, a function or a field or primitive - basicly whatever. All that it would be responsible would enforce that

@RequireBinding()
@Input() a: string | undefined | null;

<component-with-required-field-a></component-with-required-field-a> wold throw compile time error <component-with-required-field-a [a]="undefined"></component-with-required-field-a> wold not

I propose @RequireBinding instead of @Required as it clearly states that it is binding that is required cause without it mayby component doesn't make sense - for example form component with [formControl] input.

Alternative would be adding a flag to @Input(name, {requireBinding: true}) or @Input({requireBinding: true})

It could probably even be used with outputs as well

@RequireBinding()
@Output() clicked = new EventEmmiter();

witch would indicate that we are expecting consumer to handle that event.

RequireBinding of course would not stop users from using compoennts dynamicly without providing any inputs as it would be strictly developer friendly addition for templating only.

What makes it necessary to put it as 'RequireBinding' the input is supposed to be required? Binding would refear to HTML Binding...

muhamedkarajic avatar Nov 23 '22 17:11 muhamedkarajic

Any progress with this issue? :(

itayperry avatar Dec 08 '22 18:12 itayperry

Any progress with this issue? :(

I just wanted to ask the same :)

muhamedkarajic avatar Dec 22 '22 20:12 muhamedkarajic

What exactly about Typescript makes this not feasible? This is a template error, and it already goes through a compilation step. Can the compiler not infer a type from the Class instance and use reflect-metadata to extract Inputs so that they can be marked as required in the extracted type unless they are optional? Or perhaps the language service can do the heavy lifting here?

A bit beyond my understanding as I'm not well versed in the compiler, but Vue 3 + Volar support required props through a combination of compilation + the language service, so it's clearly feasible.

jmroon avatar Dec 29 '22 16:12 jmroon

Most (if not all?) of the typechecks in the templates are implemented as Type-check Blocks (TCBs) - specially crafted TS snippets that are then fed to the regular typescript compiler. What could a TCB checking for missing required inputs look like?

amakhrov avatar Dec 29 '22 19:12 amakhrov

Most (if not all?) of the typechecks in the templates are implemented as Type-check Blocks (TCBs) - specially crafted TS snippets that are then fed to the regular typescript compiler. What could a TCB checking for missing required inputs look like?

I honestly had no idea how it was implemented...haven't had any luck tracking the source, so apologies if the suggestion was a bit ignorant. Would you have a link to the source handy? Haven't had much luck tracking down the template type checks.

Maybe Vue can get around it since it compiles the template down to a render function. Not sure what Angular's approach is.

jmroon avatar Dec 31 '22 17:12 jmroon

@jmroon they covered the subject in the blog post: https://blog.angular.io/how-the-angular-compiler-works-42111f9d2549

My understanding is that the TS errors that TS compiler produces for those TCBs are surfaced directly to the user as a result. Now the problem is - what kind of a TCB should it be so that the TS-specific error would make sense and be clear to the end user?

Yeah, I can see how it's different in Vue. In Angular, a component is a class, and the compiled template is basically instantiating this class and assigns its input properties one by one.

However, perhaps it could be implemented as an extended diagnostic (introduced in Angular 13)?

amakhrov avatar Jan 04 '23 23:01 amakhrov

What could a TCB checking for missing required inputs look like?

Naively I would assume given a component or directive, create an interface for that component containing only the required inputs (typed any), and try to assign an object with only the properties specified in the using template set to an arbitrary value, so that TS2741 ("Property x is missing") is raised. I.e.,

@Component({selector: 'app-hero', template: '...'})
export class HeroComponent {
  @Required() @Input()
  heroes: Hero[];
  
  @Input()
  label: string;
}

@Component({selector: 'app-hero-user', template: '<app-hero [label]="label"></app-hero>'})
export class HeroUserComponent {...}

would become a type check

interface HeroComponent_req {
  heroes: any;
  label?: any;
}

const _heroUserComponentRequired: HeroComponent_req = {label: null}; // error: "TS2741: Property 'heroes' is missing in type '{ label: null; }' but required in type 'HeroComponent_req'."

However, is a type check even needed? For validating required inputs are present all that needs to be known is if they are "mentioned" on the use site. So the logic becomes:

  1. Enumerate a component's or directive's required inputs.
  2. Enumerate the bound inputs or attributes at the use site (regardless of their value, because typechecking is another step).
  3. Raise an error if 1 is not a subset of 2.

Is that not feasible with ngc?

grubeninspekteur avatar Feb 14 '23 07:02 grubeninspekteur