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

feat(forms): make form controls strictly typed #40772

Closed
wants to merge 3 commits into from

Conversation

MikeJerred
Copy link
Contributor

@MikeJerred MikeJerred commented Feb 9, 2021

Closes #13721

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Feature

What is the current behavior?

Issue Number: #13721

What is the new behavior?

Type information is now available on all form controls and validators.

There is an additional control called FormSection which is very similar to FormGroup except that controls cannot be added or removed at runtime, so it can contain different types of control and still have strong type information. I have not implemented an equivalent for FormArray, but this could be done if there is a use case (perhaps call it FormTuple?).

Types should for the most part be inferred automatically by typescript when constructing the control (or using the FormBuilder).

Examples:

const c = new FormControl('test');
// types:
// c: FormControl<string>
// c.value: string | null
// c.valueChanges: Observable<string | null>

const a = new FormArray([
  new FormControl('value'),
  new FormControl('other value')
]);
// types:
// a: FormArray<FormControl<string>>
// a.value: (string | null)[]

const a2 = new FormArray([
  new FormControl('value'),
  new FormControl(5)
]);
// types:
// a2: FormArray<FormControl<string> | FormControl<number>>
// a2.value: (string | number | null)[]

const a3 = new FormArray<FormControl<string>>([
  new FormControl('value'),
  new FormControl(5)
// Error: Type 'FormControl<number>' is not assignable to type 'FormControl<string>'
]);

const g = new FormGroup({
  one: new FormControl('value'),
  two: new FormControl(10)
});
// types:
// g: FormGroup<FormControl<string> | FormControl<number>>
// g.value: { [key: string]: string | number | null }

const s = new FormSection({
  name: new FormControl<string>(),
  details: new FormSection({
    size: new FormControl<'small' | 'medium' | 'large'>(),
    weight: new FormControl<number>()
  }),
  locations: new FormGroup({
    usa: new FormSection({
      cost: new FormControl<number>(),
      count: new FormControl<number>()
    }),
    japan: new FormSection({
      cost: new FormControl<number>(),
      count: new FormControl<number>()
    })
  })
});
// types:
// s.controls.details.controls.size.value: 'small' | 'medium' | 'large' | null
// s.controls.locations.controls['usa'].controls.cost: FormControl<number>
// s.value.locations['japan'].cost: number | null

Does this PR introduce a breaking change?

  • Yes
  • No

All places where a FormControl, FormArray, or FormGroup are constructed must be replaced with FormControl<any>, FormArray<any> or FormGroup<any> respectively to ensure that existing code will not cause typescript errors, .e.g:

const g = new FormGroup({
  foo: new FormControl('test')
});
g.addControl('bar', new FormControl(5));
// Error: Argument of type 'FormControl<number>' is not assignable to parameter of type 'FormControl<string>'

const c = new FormControl({foo: 10});
c.setValue({foo:15, bar: 1});
// Error: Argument of type '{ foo: number; bar: number; }' is not assignable to parameter of type '{ foo: number; }'

Alternatively, the controls in this PR could be renamed to something else (e.g. TypedFormControl) and then have FormControl be an alias for TypedFormControl<any>, in which case this PR would not introduce a breaking change.

Other information

@destus90
Copy link
Contributor

@MikeJerred
The work is already underway in this area #38406

@AndrewKushnir
Copy link
Contributor

Hi @MikeJerred,

Thanks for creating this PR! Improvements to the Form types is an important topic and it's a part of Angular Roadmap.

As @destus90 mentioned, there is a couple PRs with type improvements: #38406 (which updates types on existing classes) and #38906 (which adds a layer on top of existing APIs). There is no final decision on the approach yet and there is also a potential of (partially) combining the mentioned PRs (and may be also the changes from this PR too!). Even though there are other open PRs with type improvements, I'd like to keep your PR open as well and we'll review as a part of the effort to improve Forms typings. There is no ETA at this moment, so it may take some time for us to get back with review/comments.

Thank you.

@AndrewKushnir AndrewKushnir added area: forms cross-cutting: types feature Issue that requests a new feature labels Feb 10, 2021
@ngbot ngbot bot modified the milestone: Backlog Feb 10, 2021
@MikeJerred
Copy link
Contributor Author

Sounds good, here are some of the main differences from the existing PRs to make it easier to review:

const g = new FormGroup({
  foo: new FormControl('value'),
  bar: new FormControl(1)
});
g.removeControl('bar');
// g.value is now { foo: 'value' }
// but the type of g.value is { foo: 'value', bar: 1 }
  • feat(forms): add strict reactive forms api #38906 doesn't have the above problem because it requires the type arguments to be explicitly specified, and only allows adding/removing controls for optional keys within that specified type. In this PR the user will not usually need to specify types explicitly, since the problem is solved a different way (namely by adding a new control FormSection which cannot add or remove controls, and having FormGroup act like FormArray in that each element has the same type, freely allowing controls to be added/removed).
  • Both Experimental: Strictly typed reactive forms  #38406 & feat(forms): add strict reactive forms api #38906 do not add types to the validators and form builder, which I think is needed for this feature to be really complete.

This commit adds type arguments to all form controls and validators.

BREAKING CHANGE: some methods called on `FormControl`, `FormArray`, or `FormGroup`
may now cause a typescript error.

Calling methods such as `FormControl.setValue` with a parameter of a different type to what was
passed in the constructor of the control will cause a type error.
Migration is possible by changing all instances of `FormControl` to `FormControl<any>`,
`FormArray` to `FormArray<any>`, and `FormGroup` to `FormGroup<any>`.

Closes angular#13721
@Harpush
Copy link

Harpush commented Jul 10, 2021

@MikeJerred I like the idea of FormSection. I believe in FormGroup should become the same as Record - meant for "dictionary" based forms where keys are unknown or all optional.

@dylhunn
Copy link
Contributor

dylhunn commented Nov 4, 2021

Thank you for this great PR. In particular, your control-types approach is instructive, and I am still referencing your typings for Validator.

I am closing this PR because it is obviated by #43834. Thanks for the groundwork, and we will acknowledge your contribution in the changelog when Typed Forms lands.

@dylhunn dylhunn closed this Nov 4, 2021
@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 Dec 5, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reactive forms are not strongly typed
5 participants