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

Reactive forms are not strongly typed #13721

Closed
asfernandes opened this issue Dec 30, 2016 · 147 comments
Closed

Reactive forms are not strongly typed #13721

asfernandes opened this issue Dec 30, 2016 · 147 comments

Comments

@asfernandes
Copy link

@asfernandes asfernandes commented Dec 30, 2016

[x] feature request
  • Angular version: 2

Reactive forms is meant to be used in complex forms but control's valueChanges are Observable<any>, which are totally against good practices for complex code.

There should be a way to create strongly typed form controls.

@Toxicable
Copy link

@Toxicable Toxicable commented Dec 31, 2016

related #11279

@asfernandes
Copy link
Author

@asfernandes asfernandes commented Jan 1, 2017

This is not related to #11279.

@Toxicable
Copy link

@Toxicable Toxicable commented Jan 1, 2017

Please explain how it is not related?
What you want is for Abstract Control to be Generic right? That is the only way that valueChanges can have a type that is not Observable<any> there would be no other way to infer the type.
Which is exactly what #5404 is asking, which means this is related to #11279
If there is another way this could be implemented without making AbstractControl a generic please explain that.

@asfernandes
Copy link
Author

@asfernandes asfernandes commented Jan 1, 2017

Using get<Type> as in #11279 is definitively wrong solution. If TypeScript had somethign like Java Unbounded Wildcard, get would use it, and not any. Maybe something can be done in the same manner with a empty interface?

There is also https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html keyof. TypeScript 2.1 features may be very interesting to study to implement strongly typed form controls.

The way it is currently, unfortunately, I do not thing it's usable for large app and I need to design something on top of it.

@Toxicable
Copy link

@Toxicable Toxicable commented Jan 9, 2017

I just noticed that in TS 2.2 (https://github.com/Microsoft/TypeScript/wiki/Roadmap#22-february-2017) that they have planned Default Generic Types (microsoft/TypeScript#2175) once we have this I think it might be a good idea to revisit this issue since we could make AbstractControl generic like AbstractControl<T = any> where the T is the type of the value returned by valueChanges which would be an Observable<T>. It would not be a good idea to do it currently since it would a massive breaking change but with default generics, unless I misunderstand them, it will not be a breaking change.

@Toxicable
Copy link

@Toxicable Toxicable commented Feb 19, 2017

Small update on this, looks like Default Generics have been been moved to TS2.3 . So with the support of TS 2.1 by Angular with version 4.0 it should not be long after that before they're able to support TS 2.3 which is now when we should wait to revisit this.

@desfero
Copy link

@desfero desfero commented May 15, 2017

TypeScript 2.3 with Default Generic Types is already here, do we have any plans when support for TS 2.3 in angular will be ready?

@Toxicable
Copy link

@Toxicable Toxicable commented May 15, 2017

@desfero waiting on #16707 for the build to be upgraded to TS2.3

@kemmis
Copy link

@kemmis kemmis commented Aug 10, 2017

+1 would love to see this feature. Anybody working on it?

@Toxicable
Copy link

@Toxicable Toxicable commented Aug 10, 2017

#16828
Currently blocked on microsoft/TypeScript#16229

@rpbeukes
Copy link

@rpbeukes rpbeukes commented Oct 30, 2017

This might be of use - Angular Typesafe Reactive Forms

@Toxicable
Copy link

@Toxicable Toxicable commented Oct 30, 2017

Small update on this:
As per my comment here: #16828 (comment)
implementing generics into the current Forms API is not possible without breaking changes.
So either breaking changes are needed
Or a full forms rewrite

@Toxicable
Copy link

@Toxicable Toxicable commented Oct 31, 2017

So turns out my previous comment was incorrect.
I was able to implement this on the current Forms API as you can see here #20040

@howiempt
Copy link

@howiempt howiempt commented Oct 31, 2017

@Toxicable that still has the problem of lacking the ability to refactor safely. get('person') for example, is not really using the symbol itself. The example above, from @rpbeukes, has a retrofitted way of basically using the object symbol eg. get(obj.person) without using the string. That would be preferred over just having return types.

@Toxicable
Copy link

@Toxicable Toxicable commented Nov 1, 2017

@howiempt

get('person') for example, is not really using the symbol itself

I have no idea what you mean by this, what symbol are you referring to here?
In my implementation you can do something like

let g = new FormGroup({
  'name': new FormControl('Toxicable'),
  'age': new FormControl(22),
})

g.get('name') //AbstractControl<string>
g.get('age') //AbstractControl<number>

get(obj.person) without using the string

This lacks the ability to traverse multiple FormGroups.
While my method is unable to infer the types in this scenario the idea of my PR is to add Generic types without breaking changes or introducing any new API's (aside from generics)

@howiempt
Copy link

@howiempt howiempt commented Nov 1, 2017

@Toxicable I understand you're change is meant to not break things, not trying to criticize your solution. The other implementation (retrofitted) allows an actual property to be used rather than a string. By referencing the the field by string, if that property name changes, builds break, which for me isn't very safe. For example, changing the field name from 'name' to 'firstName', would break if I didn't change all the g.get('name') references. If I could do something like

class PersonDetails {
  name: string;
  age: number;
}
let g = new FormGroup<PersonDetails>({
  name: new FormControl('Toxicable'),
  age: new FormControl(22),
})

g.get(name) //AbstractControl<string>
g.get(age) //AbstractControl<number>

They would all be tight references. The retrofit solution does it in a slightly hacky way but does solve that problem as well.

@rpbeukes
Copy link

@rpbeukes rpbeukes commented Nov 1, 2017

@Toxicable thanx for the PR. Looking forward using it :)

I do agree with @howiempt, if we can get something like this it would be first prize:

g.get(x => x.name) //AbstractControl

Again, I don't really know how feasible this is within the greater scope of things.
I trust your judgement.

Keep up the good work and thanx for the quick response.

@Toxicable
Copy link

@Toxicable Toxicable commented Nov 1, 2017

I think that this method of accessing other controls is not related to adding generics.
Feel free to open another issue about it however

@howiempt
Copy link

@howiempt howiempt commented Nov 1, 2017

I really don't think that having the return type set is really "strongly typed", seems like half of the implementation required, but it is a step in the right direction.

@Quramy
Copy link

@Quramy Quramy commented Nov 30, 2017

Hi, I've released https://github.com/Quramy/ngx-typed-forms for workaround of this issue. Please check it out 😄

@nicu-chiciuc
Copy link

@nicu-chiciuc nicu-chiciuc commented Dec 1, 2017

@Quramy I tried to use your package several weeks ago and as I remember, it doesn't really do a lot of enforcement :(

@jasonaden jasonaden added freq3: high feature labels Dec 19, 2017
dylhunn added a commit to dylhunn/angular that referenced this issue Mar 22, 2022
This PR strongly types the forms package by adding generics to AbstractControl classes as well as FormBuilder. This makes forms type-safe and null-safe, for both controls and values.

The design uses a "control-types" approach. In other words, the type parameter on FormGroup is an object containing controls, and the type parameter on FormArray is an array of controls.

Issue: angular#13721
dylhunn added a commit to dylhunn/angular that referenced this issue Mar 23, 2022
This PR strongly types the forms package by adding generics to AbstractControl classes as well as FormBuilder. This makes forms type-safe and null-safe, for both controls and values.

The design uses a "control-types" approach. In other words, the type parameter on FormGroup is an object containing controls, and the type parameter on FormArray is an array of controls.

Issue: angular#13721
dylhunn added a commit to dylhunn/angular that referenced this issue Mar 23, 2022
This PR strongly types the forms package by adding generics to AbstractControl classes as well as FormBuilder. This makes forms type-safe and null-safe, for both controls and values.

The design uses a "control-types" approach. In other words, the type parameter on FormGroup is an object containing controls, and the type parameter on FormArray is an array of controls.

Issue: angular#13721
@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented Mar 25, 2022

Now that the migration is settling into a near-final state, here are some updated instructions for trying the demo with your own project:

1. Update @angular/core to version 14.0.0-next.8 or higher: ng update @angular/cli @angular/core --next
2. In package.json, switch @angular/forms from 14.0.0-next.x to https://dl.circleci.com/private/output/job/026a218d-c4d2-4bd9-8edd-43b5e199fdf7/artifacts/0/angular/forms-pr43834-17ce0ac4a6.tgz
3. Install the new dependencies, ignoring peer dependency warnings: npm i --force or yarn install --force. Commit all your changes so far (probably on a branch).
4. Run the typed forms experimental migration: ng update @angular/core --migrate-only --name migration-v14-typed-forms

@destus90
Copy link
Contributor

@destus90 destus90 commented Mar 28, 2022

@dylhunn

@angular/cli@14.0.0-next.5 says that —-migrate-only option no longer accepts a string of a migration name. Should the mention command be rewritten as ng update @angular/core --migrate-only --name migration-v14-typed-forms?

@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented Mar 28, 2022

@destus90 Probably! Looks like the CLI team changed the flags out from under me :)

@ViktorPlohl
Copy link

@ViktorPlohl ViktorPlohl commented Apr 1, 2022

@dylhunn All I want to say is that I’ve been waiting for this change for years. I tried it yesterday and it is very good so far. Thanks!

@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented Apr 12, 2022

PR #43834 merged the new types, and is released in 14.0.0-next.12. The next step is to land FormRecord, and then fix any bugs that arise during the next and RC periods.

You can now try out typed forms by switching to the next channel: ng update @angular/cli @angular/core --next

@e-oz
Copy link

@e-oz e-oz commented Apr 13, 2022

I've tried new typed forms (Angular 14.0.0-next.12, TS 4.6.3 and TS 4.7.beta), and right now I don't quite understand how to use types with them.
2 examples:

  1. I have a FormGroup with the field "text", type is "string". To declare it, I need to write:
    form = new FormGroup<{ text: FormControl<string | null> }>({text: this.textCtrl});
    I expected it will be:
    form = new FormGroup<{ text: string }>({text: this.textCtrl});
    but even this is not allowed:
    form = new FormGroup<{ text: FormControl<string> }>({text: this.textCtrl});
    because "FormControl" should be replaced with "FormControl<string | null>".

  2. I have a complicated structure of type T, for this structure I create a FormGroup. Right now it's impossible to just declare this:
    form = new FormGroup<T>({});
    TS requires to declare an AbstractControl for every field of that big complicated structure.

Is it how it's supposed to work?

@e-oz
Copy link

@e-oz e-oz commented Apr 13, 2022

One more question: how to remove a control now?

const oldAttrCtrls = this.attrsForm?.controls ? Object.keys(this.attrsForm.controls) : [];
if (oldAttrCtrls?.length) {
  oldAttrCtrls.forEach((attr) => {
    // 👇
    this.attrsForm.removeControl(attr); // 🛑 TS2769: No overload matches this call.  ... (long explanation here, because this form has a complicated type)
   //  👆
  });
}

The only workaround I've found is // @ts-ignore

@e-oz
Copy link

@e-oz e-oz commented Apr 13, 2022

One more thing:
formGroup.get(someStringValue) still returns an AbstractControl, even if formGroup is fully typed (so no AbstractControls are expected). So in templates $any() is still needed (it's sad).

@e-oz
Copy link

@e-oz e-oz commented Apr 13, 2022

Sorry for posting a lot, but one more thing: TypeScript goes crazy when it has to parse code with some typed FormGroup. CPU skyrockets. It's just my wild assumption, but looks like there are some endless recursions.

@fen89
Copy link

@fen89 fen89 commented Apr 13, 2022

One more question: how to remove a control now?

const oldAttrCtrls = this.attrsForm?.controls ? Object.keys(this.attrsForm.controls) : [];
if (oldAttrCtrls?.length) {
  oldAttrCtrls.forEach((attr) => {
    // 👇
    this.attrsForm.removeControl(attr); // 🛑 TS2769: No overload matches this call.  ... (long explanation here, because this form has a complicated type)
   //  👆
  });
}

The only workaround I've found is // @ts-ignore

@e-oz Given your form consists of the following:

interface Person {
  firstName: string;
  lastName: string;
}

const form = this.fb.group<IPerson>({
  firstName: '',
  lastName: '',
});

Typescript is complaining that the argument given of type string is not assignable to any of your forms parameters => Argument of type 'string' is not assignable to parameter of type '"firstName" | "lastName"'.

1.) Casting the function to type any

form.controls.forEach((attr) => {
  form.removeControl<any>(attr); 
});

2.) Casting the string attr to keyof Person

form.controls.forEach((attr: keyof Person) => {
  form.removeControl(attr);
});

@e-oz
Copy link

@e-oz e-oz commented Apr 13, 2022

Thanks, @fen89 , but my form has type Record<string, ComplicatedTypeHere>, so it should have string keys.

@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented Apr 13, 2022

@e-oz

form = new FormGroup<{ text: string }>

We are using control types -- rely on the inference, or try FormGroup<{text: FormControl<string>}>.

"FormControl" should be replaced with "FormControl<string | null>".

You need to add the following option to your constructor: {initialValueIsDefault: true}.

TS requires to declare an AbstractControl for every field of that big complicated structure.

This is a use case for FormRecord as proposed in the RFC! FormRecord isn't quite ready yet, but it should land sometime next week -- it will support dynamic groups with homogenous keys and values, i.e. exactly the use case you're requesting.

how to remove a control now?

Declare the control as optional (FormGroup<{foo?: FormControl<string>}>) or use FormRecord (once it's available soon!)

formGroup.get(someStringValue) still returns an AbstractControl

This one is tricky to fix, unfortunately. I'm working on it, I'll post back with updates.

@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented Apr 13, 2022

but my form has type Record<string, ComplicatedTypeHere>

@e-oz This is a perfect use case for FormRecord, stay tuned a few more days :)

(You could also use an index signature: FormGroup<{[key: string]: ComplexTypeWithControls}>, which does the same thing)

@e-oz
Copy link

@e-oz e-oz commented Apr 21, 2022

More updates from me.

With the time I've found ways how to use new forms more efficiently, and I can't imagine how sweet it will be with FormRecord, because it's pretty sweet already right now :) Thank you very much for implementing this feature!

  1. The most handy thing is the ability to use [formControl]="form.controls.someName" in the templates - it's so good, you need to try it.

  2. Issue with CPU skyrocketing is localized, and it was caused by the use of "removeControl"/"addControl". It can be resolved pretty simple: just downcast form's type to <any>:

CPU-heating version:

const oldAttrCtrls = this.attrsForm?.controls ? Object.keys(this.attrsForm.controls) : [];
if (oldAttrCtrls?.length) {
  oldAttrCtrls.forEach((attr) => {
    // @ts-ignore
    this.attrsForm.removeControl(attr);
  });
}

CPU-polite version:

const form = this.attrsForm as FormGroup<any>; // temporary trick
const oldAttrCtrls = form?.controls ? Object.keys(form.controls) : [];
if (oldAttrCtrls?.length) {
  oldAttrCtrls.forEach((attr) => {
    form.removeControl(attr);
  });
}

Thanks again, I love to use new forms :)

@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented Apr 23, 2022

@e-oz Are you using IntelliJ/WebStorm? The crazy CPU usage is caused by a bug in WebStorm's TypeScript integration (which is not the standard language server; it's unique to WebStorm). We are working with Jetbrains currently, and hope to have it resolved soon.

If you're using another IDE, that's concerning and I'd love to hear details.

@e-oz
Copy link

@e-oz e-oz commented Apr 23, 2022

@dylhunn I use IntelliJ IDEA (not Webstorm, but from the same family). I've checked the code in VS Code and it works fine ("TypeScript: Tsdk" setting is set to "./node_modules/typescript/lib").

@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented May 19, 2022

The performance issues with Jetbrains IDEs (Webstorm, IntelliJ) should be fixed in version 2022.1.1. Let us know if any issues are still occurring at that version or later.

@gparlakov
Copy link
Contributor

@gparlakov gparlakov commented May 27, 2022

Hey all, here's a drop-in alternative while waiting for this issue to be resolved.

npm package: ngx-forms-typed - https://www.npmjs.com/package/ngx-forms-typed

@dylhunn
Copy link
Contributor

@dylhunn dylhunn commented Jun 2, 2022

Hi everyone! I'm thrilled to say that typed reactive forms will be landing today in Angular 14. If you discover any bugs, please open a new issue to report them. Although there are some more improvements we'd like to make in the future, such as changes to template type checking, we will track those on a different issue.

@dylhunn dylhunn closed this as completed Jun 2, 2022
Feature Requests automation moved this from In Progress to Closed Jun 2, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment