Canonical: Automatically generate Forms control types from model type
See original GitHub issueWhich @angular/* package(s) are relevant/related to the feature request?
forms
Description
Abstract
In the following, I will propose a helper to pass API-like interfaces to strictly typed FormGroup
in order to not have to rewrite types. Also, this propose could also fix a bug affecting FormBuilder
which returns the wrong type when dealing with objects
Details
When building a strictly-typed FormGroup
, it expects an interface having already the right types defined for its properties, i.e.:
interface SomeInterface {
foo: string;
bar: {
baz: number;
}
}
interface SomeInterfaceForm {
foo: FormControl<string>;
bar: FormGroup<{
baz: FormControl<number>;
}>;
}
However, this is a limitation when dealing with API, because it forces to duplicate interfaces in order to define the right types for FormGroup
. Also, this may be the source of possible bugs because changes in API’s data should be reflected also in form interface.
Moreover, if using FormBuilder
, the resulting type is not correctly inferred:
const fg = formBuilder.group<SomeInterface>({
foo: '',
bar: {
baz: 0
}
});
fg.controls.bar.controls // Error: Property 'controls' does not exist on type 'FormControl<{ baz: number; }>'
Proposed solution
I propose to create and expose a helper type able to infer the correct type given its input, something like the following:
type Controls<T> = { [k in keyof T]: Form<T[k]> }
type Form<T> = [T] extends [boolean | number | string | null | undefined] ? FormControl<T> :
[T] extends [(infer U)[]] ? FormArray<Form<U>> :
FormGroup<Controls<T>>
and apply it to FormGroup
like:
class FormGroup<T> {
constructor(public controls: Controls<T>) {
// ...
}
}
In this way, one can define a FormGroup
starting from API interface in this way:
const fg = new FormGroup<SomeInterface>({
foo: new FormControl<string>('')
// ...
}):
having back SomeInterface
when accessing fg.value
(related to #46073)
Alternatives considered
None
Issue Analytics
- State:
- Created a year ago
- Reactions:43
- Comments:10 (4 by maintainers)
Top GitHub Comments
It would be really nice if Forms could automatically infer the model types from your data types. The reason we didn’t include this in v14 is that, unfortunately, it doesn’t work in the general case. Here’s why:
Consider you have a data model that represents a date:
This could correspond to a form with three fields:
Or it could correspond to a datepicker widget, which is a single custom control which returns an object:
And it’s impossible to know which generally!
It would be nice to make improvements in this area, but this general issue would need to be designed around.
Let me show you a solution by using Typescript utilities that might be helpful in some situations.
In case you can’t initialize your FormGroup in the constructor, becase you need some data coming from @Input (for example), there is a way that Typescript Utilities can help.
Imagine the following scenario:
The type inferral of the FormGroup doesn’t work because the initialization is not done in the declaration. Now, let’s do our magic.
First, encapsulate the FormGroup initialization into an isolated function, like this:
You can check that the resulting FormGroup type is properly inferred.
And then, by using ReturnType utility (see https://www.typescriptlang.org/docs/handbook/utility-types.html#returntypetype) we’ll indicate in the declaration which type the formgroup is:
Now you can check that the “formGroup” property has the correct type inferred! This might be very useful for large FormGroups or when using Interfaces.
I hope can be useful for you too!