-
-
Notifications
You must be signed in to change notification settings - Fork 56
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
feature: support exact control types in FormGroup and FormArray #35
feature: support exact control types in FormGroup and FormArray #35
Conversation
Support generic type that contains the type of the controls, so it's possible to infer it properly.
validatorOrOpts?: ValidatorOrOpts, | ||
asyncValidator?: AsyncValidator | ||
) { | ||
constructor(public controls: ControlsOfValue<T>, validatorOrOpts?: ValidatorOrOpts, asyncValidator?: AsyncValidator) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will it still be typed for nested forms that includes FormGroup
or FormArray
without passing the FormGroup
or FormArray
types?
for example:
interface ProfileControls {
firstName: string;
lastName: string;
address: {
street: string,
city: string
}
}
const profileForm = new FormGroup<ProfileControls>({
firstName: new FormControl(''),
lastName: new FormControl(''),
address : new FormGroup({
street: new FormControl(''),
city : new FormControl('')
})
});
will city and still be typed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in that case the form controls will be interpreted as:
{
firstName: FormControl<string>;
lastName: FormControl<string>;
address: AbstractControl<{
street: string;
city: string;
}>
}
Which means that:
profileForm.getRawValue().address.city; // string
profileForm.getControl('address'); // AbstractControl<{street: string; city: string}>
profileForm.getControl('address', 'street'); // AbstractControl<string>
Exactly as it was before these changes.
* | ||
* The intermediate type is to solve the issue of T being any, thus assignable to all condition and resulting in the "any" type. | ||
* */ | ||
type ControlOfValueWithPotentialUnion<T> = T extends AbstractControl ? T : T extends number | string ? FormControl<T> : AbstractControl<T> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about boolean
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. Should probably add null and undefined too. (for types like number | null
, for example)
Fixed.
* phone: new FormControl<{num: number, prefix: number}>(), | ||
* }); | ||
* */ | ||
export type FlatControls<T extends Object> = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could you add some more tests and examples for it?
for example, add a test for nested form and use getControl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a small test to this, and added this to the documentation.
@@ -62,11 +62,60 @@ export type KeyValueControls<T extends Obj> = { | |||
[K in keyof T]: T[K] extends FormControl<T[K]> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does KeyValueControls
still in use here? or we can delete it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, it's not being used.
Removed.
@sharshuv-quotient thanks for the quick reaction. There is one more thing I would like to ask you to do, could you please run your branch on some big project to make sure there are really not breaking changes? |
Unfortunately I don't have a big project already using this library. |
@sharshuv-quotient sure we will also validate it from our side, though it would be nice if you could also try to test your branch inside your project (you could use npm link for that...). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we also have this support in form builder?
Yes we can, but I think that's outside of the scope of this PR, since this can be merged without adding this to the form builder too. |
…m-group-and-array-typing1 # Conflicts: # projects/ngneat/reactive-forms/src/lib/formGroup.ts # projects/ngneat/reactive-forms/src/lib/types.ts
@itayod Are you currently checking this on your project? |
@sharshuv-quotient could you resolve the failing test? |
@itayod Fixed. |
I'm not sure about this solution. You eliminate the need for casting, but you're also forcing me to override my model interface. In most cases, I'll have my model defined, for example - |
I'm not forcing you to use a different interface. As I said, this is completely backward compatible. I don't see it as a duplicated interface, but as a different interface giving different information. If you want the controls interface to be connected to the value interface in some way (and avoid duplication as much as possible), you can do something like that: interface Profile {
firstName: string;
lastName: string;
skills: string[];
address: {
street: string;
city: string;
};
}
interface ProfileControls extends ControlsOf<Profile> {
skills: FormArray<string>; // overriding AbstractControl<string[]>
address: FormGroup<Profile['address']> // overidding AbstractControl<Profile['address']>
}
const fg = new FormGroup<ProfileControls>(...) |
Adding my two cents. I think the ideal goal is for the type system to understand that if you pass interface T, then it can map each key to a form control, form array or form group based on it's type signature. export interface MyInterface {
key1: string; // Becomes FormControl<string>
groupKey: { nestedKey: string } // Becomes FormGroup<{ nestedKey: FormControl<string> }>
arrayKey: string[]; // Becomes FormArray<string>
} We don't exactly want the user to have to create a new interface to define this, we should be able to build this interface for them using the type system. Otherwise we could simply tell users to cast their form value:
|
@Coly010 exactly. But there is one exception here. You can use an object or an array with a |
A reasonable solution to solve this issue without introducing breaking changes is to create something like: const fg = new FormGroup<ControlsOf<Profile>>(...) |
In those instances, it may be worth telling the user to provide an override for the keys that don't map based on type signature? |
Yes, but from my experience, it's not too common to pass an object/array to a FormControl, so I'll be OK with this. |
@danzrou any thoughts? |
The only use-case i can think of is a multi-select form control. That returns an array of values for one FormControl right? |
Usually, it will be a custom control accessor that takes an object/array. But again, not so common. |
@Coly010 Correct me if I'm wrong, but what you are suggesting here is make the default interpretation of the value type regard object as a FormGroup and array as FormArray?
@NetanelBasal You mean here the |
I mean that interface ProfileControls extends ControlsOf<Profile> {
skills: FormArray<string>; // overriding AbstractControl<string[]>
address: FormGroup<Profile['address']> // overidding AbstractControl<Profile['address']>
} |
OK, I'll add it, but I think it should be named |
I'll add my two cents here. While developing this lib, we did try handling this by providing a type which will checkup the return values (@NetanelBasal That said, our main goal was to be able to provide this lib seamlessly with almost no effort so users could hook up and integrate it quickly and with minimum of writing. As most of the solutions provided here are decent, they seem (at least from a first sight) to have a potential of not keeping things plain and simple as intended and adding (maybe indeed minimal) boilerplate for the user. However, unfortunately, after doing a lot of thinking and also messing around a bit with strict types (#26) I couldn't yet think of a better option than providing the controls type explicitly by the user. That's why I would go with the most simple solution advised here - a utility type that will resolve the values to their "natural" type:
|
Prevent distributive conditional types so ControlOfValue<boolean> is FormControl<boolean> and not FormControl<true> | FormControl<false> See microsoft/TypeScript#37279
Hey, I've added the discussed interface, which can be used optionally. I did not implement the Overrides feature since I believe this should be a separate feature in a separate PR. |
…m-group-and-array-typing1 # Conflicts: # src/app/app.component.ts
Resolved conflicts. |
@sharshuv-quotient as we wrote above we wanted this pr to include only the |
* rename DeepControlsOf to ControlsOf as requested * Added the ability to override the default behavior of this type.
Done. |
Hi @sharshuv-quotient the In other words, try focus only on the |
The other utility you are referring to is |
correct, just remove the |
…y-typing1 # Conflicts: # projects/ngneat/reactive-forms/src/lib/formGroup.ts
Resolved conflicts and remove FlatControls interface as discussed. |
Resolved failing tests |
Hey, can we merge this? |
Hi @sharshuv-quotient sorry for the late response... could you resolve those conflicts and revert the "doc" changes that are not related to this feature? after you do so I think we can merge it :D |
…m-group-and-array-typing1 # Conflicts: # projects/ngneat/reactive-forms/src/lib/formArray.ts # projects/ngneat/reactive-forms/src/lib/formGroup.ts
Seems to be like TypeScript needed a little help inferring some of the types.
Resolved conflicts. The doc seemed to be up to date though. Can you please point out what exactly is wrong? |
LGTM |
@sharshuv-quotient well done! |
* feature: support exact control types in FormGroup and FormArray Support generic type that contains the type of the controls, so it's possible to infer it properly. * fix(pr): form group controls type * fix: conflicts * fix: test Add explicit generic type to avoid type error in formGroup.spec.ts * fix(type): solve issue with union types Prevent distributive conditional types so ControlOfValue<boolean> is FormControl<boolean> and not FormControl<true> | FormControl<false> See microsoft/TypeScript#37279 * feat: deep-controls interface * feat: override group controls type * rename DeepControlsOf to ControlsOf as requested * Added the ability to override the default behavior of this type. * fix(types): remove flat controls interface * fix(test): stabilize tests * fix(test): stabilize tests * fix(conflict): resolve conflicts * fix(tests): fixed tests typing Seems to be like TypeScript needed a little help inferring some of the types. Co-authored-by: sharshuv-quotient <[email protected]>
@ShacharHarshuv I have noticed there is an issue that seems to be due to this pr. interface User {
id: number;
}
let user: User;
const form = new FormGroup({
id: new FormControl(user.id)
});
// The type of control should be FormControl<number, any> instead it is AbstractControl<number | FormControl<number, any>>.
const control = form.getControl('id'); // AbstractControl<number | FormControl<number, any>> |
PR Checklist
Please check if your PR fulfills the following requirements:
PR Type
What kind of change does this PR introduce?
What is the current behavior?
From the current documentation:
"Note that the return type should still be inferred." - This PR is fixing this issue.
What is the new behavior?
I'm enabling passing the type of the control / controls in addition to the current ability to pass the value type to
FormGroup
orFormArray
.For example, this way I can differ between those two, otherwise identical, Form Groups:
Now I'll get automatic inference for the controls type:
Does this PR introduce a breaking change?
Note those changes are completely backward compatible! For evidence, I did not change existing tests and they all pass.