Replies: 34 comments 80 replies
-
Great idea! It would help a lot. About this:
I hope you'll mark it as a breaking change. Because I see already how this change would mess up a lot of the code where I expect "reset" to set nulls. It might lead to very cruel bugs, especially when forms are complicated (or have multiple levels of sub-groups). I see your reasoning and I support it, but it will be a completely new behavior, so would be nice to use in the new code only, without removing the old behavior. |
Beta Was this translation helpful? Give feedback.
-
Thank you, Dylan (and the Angular team) for this RFC. 1. Is there a compelling use case for tuple-typed FormArrays? I have not seen one compelling use-case for tuple-typed FormArrays thus far. When it comes to different values/controls in FormArray, I usually reach for FormArray of FormGroup instead and the group contains more information to differentiate the value types rather than using tuple-type.
2. Is there a compelling use case for a map style FormGroup? 100%. Dynamic Forms is probably one of the reasons to use Reactive Forms in the first place. 3. Is the current nullability of FormControls useful? I personally am aware of this and always reset to default values (we do have to keep track of the default values via a class property). So to me, it is not useful at all. About this RFC which proposes to have
4. Are the limitations involving control bindings a blocking issue? I can see it as an annoyance that can be worked around with some strongly-typed pipes. But yeah, follow-up improvement with the Template Type Checker would be nice |
Beta Was this translation helpful? Give feedback.
-
On this question:
I currently maintain a medium-sized enterprise application with some fairly involved reactive forms, and I believe handling this issue in some follow-up update in the future is perfectly fine. I would take whatever I could get on the TS side as soon as possible and worry about the template bindings side later. |
Beta Was this translation helpful? Give feedback.
-
YES PLEASE!!!!!!!!!!! |
Beta Was this translation helpful? Give feedback.
-
Hello, Thanks for the awesome RPC and feature. I wanted to ask a question about nullability of form controls. In current scenario, the returned FormControl from calling its constructor is type | null. We type our froms strongly with @ng-neat/reactive-forms. They return non-null types by default and we architected our code based around not having nulls on our models. We considered slowly migrating to official typed forms, but since new FormControl returns a nullable type, this would break A LOT OF our use cases(we use request types for transforming the model data to actual requests and those types are non-nullable mostly). And doing this in a 20-25 field model would get very tiresome really quickly: Would it be possible to set this value on the from group that the form controls reside in, and can that turn all the form controls inside the group non-nullable types? Or maybe provide another FormControl that returns non-nullable types(We can do this on our own though, no need to extend the API 🤔) I don't know just spitting ideas :) I'm just trying to find a solution to a problem that we have, I don't know the angular base at all, just wanted to share a use case where nullable form control types can be annoying and hard to migrate. Cheers! |
Beta Was this translation helpful? Give feedback.
-
The proposal looks good, but I have a question around a specific implementation. If we give the property of the type MyFormType = {
age: number;
}
const formGroup = new FormGroup<MyFormType>({
age: new FormControl(0),
});
###
<input type="number" [formControlName]="age" /> When we perform I've encountered problems in the past where I've expected the type to be a number, but because it has come from an input box on the HTML, it is still a string (as read from |
Beta Was this translation helpful? Give feedback.
-
First of all - a great thing to finally have built in typed reactive forms! I am very excited to use it.
|
Beta Was this translation helpful? Give feedback.
-
Thank you for this awesome RFC 💪
It might be an idea to provide an extra option for the Existing forms found:
- Would like to apply strong types for your existing forms? (might cause breakage): Y/n
See: https://angular.io/docs?explain-how-why-etc |
Beta Was this translation helpful? Give feedback.
-
Your work looks great, thank you. This is what I think about this topic: (I didn't read the other comments, so please forgive me if you answered some already)
interface Profile {
firstName: string;
lastName: string;
address: {
street: string;
city: string;
};
}
const profileForm = new FormGroup({
firstName: new FormControl(''),
lastName: new FormControl(''),
address: new FormGroup({
street: new FormControl(''),
city: new FormControl('')
})
}); I expose a
// root.component
class Root {
group = new FormGroup({})
}
// child.component
class Child {
constructor(root: Root) {
root.addControl('key', new FormControl(''))
}
} Moreover, dynamic forms libraries such as formly are worth looking at.
|
Beta Was this translation helpful? Give feedback.
-
I understand that this is probably a much heavier breaking change than you want to take but I feel like almost all of the Having an implementation of a strongly typed type ValueOf<T> = number extends T ? number | undefined : boolean extends T ? boolean | undefined : T;
type TypeName<T> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : never;
type TypeFromName<T> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : never;
function sanitizeBoolean(value: unknown): boolean | undefined {
if(typeof value === 'boolean') return value;
if(typeof value === 'string') {
if (value === 'true') return true;
if (value === 'false') return false;
}
return undefined;
}
function sanitizeNumber(value: unknown): number | undefined {
// ...
}
function sanitizeString(value: unknown) {
// ...
}
class FormControlTyped<T extends string|number|boolean, Name extends TypeName<T>> {
constructor(private _value: ValueOf<T>, private readonly _type: Name) { }
private typeGuard<TUnknown extends string|number|boolean, UnknownName extends TypeName<TUnknown>>(check: UnknownName): this is FormControlTyped<TypeFromName<UnknownName>, TypeName<TypeFromName<UnknownName>>> {
// without the cast, TS2367: This condition will always return 'false' since the types 'Name' and 'TypeName<TUnknown>' have no overlap.
return this._type as unknown === check;
}
get Value(): ValueOf<T> {
return this._value;
}
/**
* sets value of form control
* setting to undefined will reset to initial value
* setting to empty string will set the DOM control value to empty string (potentially an invalid control state)
*/
set Value(v: T|''|undefined) {
// ...
if(this.typeGuard('string')) {
// without the cast, TS2345 : Type 'string' is not assignable to type 'ValueOf<T> & string'.
this._value = sanitizeString(v) as any;
}
if (this.typeGuard('number')) {
this._value = sanitizeNumber(this._value) as any;
}
if (this.typeGuard('boolean')) {
this._value = sanitizeBoolean(this._value) as any;
}
throw new Error('type is invalid');
}
} capturing this type name for FormControl obviously is a much bigger breaking change than anything you are considering but the type considerations for |
Beta Was this translation helpful? Give feedback.
-
It looks like It feels redundant, as a FormArray returns an Array. Typing it |
Beta Was this translation helpful? Give feedback.
-
The migration does not seem to pick up some components. In particular, it looks like it ignores all lazy-loaded components right now. I opened an issue with a repro, see #44524 |
Beta Was this translation helpful? Give feedback.
-
To play devil's advocate, the current permissive typing enables users to opt-in to stricter typing by defining interfaces that extend the So I guess my concern is, how would these changes impact/interact with implementations like mine or |
Beta Was this translation helpful? Give feedback.
-
I really love this and can't wait to see it implemented. I'll play a bit with the example, but love everything you're thinking and how you plan to solve some issues now and in the future (i.e. reset(), default value vs null by default). |
Beta Was this translation helpful? Give feedback.
-
const dog = new FormControl('spot', {initialValueIsDefault: true});
// dog has type FormControl<string>
dog.reset();
const whichDog = dog.value; // spot It would be nice to be able to set the default value asynchronously. Something like this: const dog = new FormControl('spot'); // dog has type FormControl<string>
this.getValueFrombackend.subscribe(value => dog.setValue('beSpot', {initialValueIsDefault: true}));
// ...few moments latter...
dog.reset();
const whichDog = dog.value; // beSpot |
Beta Was this translation helpful? Give feedback.
-
Allowing to perform certain actions to controls based on the typing is a wonderful idea. However, I think we should add something more to this. Currently, controls can be added after the instantiation of a form group, that is why using I feel this the suggested approach of using non-null assertion operator has drawbacks:
Suggestion for an enhancement
This shouldn't be hard to implement in TS, yet would provide better ergonomics. Additionally, it will move Angular and this new feature into the recommended |
Beta Was this translation helpful? Give feedback.
-
As mentioned, this is surprising, and will affect the expected benefits of strict typing. After putting a lot of effort into creating models, type form controls and so on, you want to safely use the value of the form group and end up having a partial value. True, you can always use Proposal
|
Beta Was this translation helpful? Give feedback.
-
type Party = {
address: {
house: number,
street: string,
},
...
} What if
Will the html control be of type
Which type will prevail here?:
|
Beta Was this translation helpful? Give feedback.
-
Migration to
|
Beta Was this translation helpful? Give feedback.
-
I'm following the instructions and getting Artifact not found |
Beta Was this translation helpful? Give feedback.
-
In the stackblitz example reset code does not work:
After looking at the code, seems like default value has to be provided as a 4th argument, but that is not reflected in types. e.g.
Would work |
Beta Was this translation helpful? Give feedback.
-
It's possible to get non-existing control without getting any errors, e.g.
It seems like there are some checks for it, but this should not compile since the forms are not dynamic |
Beta Was this translation helpful? Give feedback.
-
I love that approach a lot. I was using @ng-stack/forms until angular has typed forms itself. There are some other 3rd party libs that try to solve the same issue. Until now feels wrong that in an Angular project everything is typed but when you come to a more complete thing like forms you are back in the untyped magic hell. Regarding Question 3 Regarding Qestion 4: |
Beta Was this translation helpful? Give feedback.
-
Thank you for this RFC and all the work that has gone into this issue. We use This design is excellent. The biggest change I would like to see is adding strongly-typed
I haven't seen one; I can't envision a case where a FormGroup wouldn't work well enough.
Yes: I like the My preference is delineating form control containers as follows:
RE
Not as implemented - the current behavior of The disabled formcontrol behavior causing
Not blocking, and I don't see how it could be - it doesn't make things worse than they are today. But it would be great to have confidence that FormControl types and CVA types match up, if you can make that work. |
Beta Was this translation helpful? Give feedback.
-
Is there a chance that the typing also allows us to query which validators a control has? A common issue is that form controls are required but someone forgets to add the * in the template. Material lib for example does not know if the reactive form control is required so you always need to set the attribute manually. |
Beta Was this translation helpful? Give feedback.
-
Hi, thank you for this RFC. It seems really great.
|
Beta Was this translation helpful? Give feedback.
-
First off, very, very GLAD to see the Angular team working on strongly typed forms! Very exciting!! Can't wait for this to land. Also appreciate the RFC. Gives me the opportunity to comment on an issue I've been facing recently with Angular 12, and this concept of expanding types with So, without further ado, one of the things that worries me about Angular, is that it has the tendency to expand types. With TypeScript strict mode, the use of null adds another factor to a type. This can cause For some background, I prefer not to use TypeScript strict mode can end up causing Finally, I tend to use TypeScript in a very "Clojure-like" way, following much of the philosophy Rich Hickey takes when it comes to software development. I started learning about Clojure several years ago, and greatly enjoyed Hickey's simple, data-oriented, and highly functional approach to solving problems that often become unnecessarily complex in other languages/platforms. I've employed a lot of his philosophy in a very functional approach to writing TypeScript, which includes preferring "non-existence" to "having this special value called null" or this concept that something "may exist, or maybe not" (both concepts, as it turns out, tend to be "infectious" to code bases in one way or another...the Maybe monad is definitively infectious). That part of his philosophy is embodied in this talk he gave some years ago: https://www.youtube.com/watch?v=YR5WdGrpoug Well worth the time, for anyone who is trying to use TypeScript in a functional manner. Wonderful stuff! Specifically speaking, time index 8:00 in the above video embodies a critical factor. Hickey states it quite clearly: An "easing" of requirements should be a "compatible change", however with TypeScript strict mode, this is no longer true when adding So, with that, I read this in the first post, and it gave me great concern: const name = cat.value.name; // type `string|null`
cat.controls.name.setValue(42); // Error! `name` has type `string|null` The types listed here are interface MyModel {
name?: string;
} In order for a model like this to be compatible with a form that always EXPANDS the natural type to include the, as of TypeScript Strict Mode, UNNATURAL option interface MyModel {
name?: string | null;
} Not only is this type expansion potentially unwanted, it adds complexity to code that should just be a simple optional property. Further, the use of null in the model, would then require the developer deal with the possible nulls elsewhere, potentially requiring Once again, the null is infectious and has to grow throughout the codebase, because the underlying framework is ADDING potentially unwanted nullable type expansions due to the currently proposed typed forms design. As with my I would always prefer the EXPLICIT option to choose to use null if and when I choose, rather than be forced to deal with them. I very strongly believe Angular should, in all cases and at all opportunities, avoid EXPANDING types to include |
Beta Was this translation helpful? Give feedback.
-
After several weeks of discussion, the Typed Forms RFC is now closed. Based on the feedback and the initial prototype, we plan to move forward with the proposed design. We’ll provide more updates as we progress with the implementation and incorporate your feedback, so stay tuned! In particular, a couple action items stand out:
Thanks to all the participants for your help evolving the Angular framework! |
Beta Was this translation helpful? Give feedback.
-
We need a way to represent the absence of a value for a form field of type number. For example, <p-inputNumber in PrimeNG accepts null and shows a blank input, or if it has a value and the value is backspaced out, returns null. Is this going to eventually be an issue? Should we all be migrating to using undefined? (I wish TypeScript would just anoint the Option type as idiomatic). |
Beta Was this translation helpful? Give feedback.
-
@the Forms package currently behaves in a very unsafe way: controls reset to null, which can violate the expected value type. Yes, unsafe and it may result in verbose serialization, because null will be presented in serialization by default. Currently I have to change all properties with null to |
Beta Was this translation helpful? Give feedback.
-
RFC: Strictly Typed Reactive Forms
Author: @dylhunn
Contributors: @alxhub, @AndrewKushnir
Area: Angular Framework: Forms Package
Posted: December 16, 2021
Status: Complete
Related Issue: #13721
The goal of this RFC is to validate the design with the community, solicit feedback on open questions, and enable experimentation via a non-production-ready prototype included in this proposal.
Complete: This RFC is now complete. See a summary here.
Motivation
Consider the following forms schema representing a party, which allows users to enter details about their very own party:
In the current version of Angular Forms, we can construct a corresponding form. Here’s such a form, populated with a default value. This default party is happening at
1234 Powell St
, is not a formal event, and has no food options:Now let's try to interact with our form. As you can see, we frequently get values of type
any
when reading it. The typeany
is far too permissive, and is very unsafe. This issue is pervasive across the entire Forms API:However, with typed forms, the types are highly specific and far more helpful:
These are much more useful types, and consumers that handle them incorrectly will get a compiler error (instead of a silent bug). For example, trying to do arithmetic on a value of a string control will now be an error:
partyForm.get('address.street')!.value + 6
.This illustrates the purpose of typed forms: the API now reflects the structure of the form and its data. These benefits should prove especially useful for very complex or deeply nested forms.
Goals and Non-Goals
Goals
Non-Goals
This is not a redesign of Forms; improvements are narrowly focused on incrementally adding types to the existing system.
Tour of the Typed API
Backwards-Compatibility
Let’s use our new API to create a
FormGroup
. As you can see, the existing API has been extended in a backwards-compatible way: this code snippet will work with or without typed forms.Once the typed forms API is rolled out, interacting with this
cat
form will be much safer than before:Existing projects may not be 100% compatible with this stricter version of the reactive forms API at launch. To avoid build breakage,
ng update
will migrate existing calls to opt out of typed forms by providing an explicitany
when constructing forms objects, thus aligning them with the current untyped semantics:This
<any>
causes form APIs to function with the same semantics as untyped forms do today, allowing for an incremental migration path where applications and libraries can gradually improve type safety without fixing every type error at once.In practice, we will add a type alias for
any
(e.g.AnyForUntypedForms
) to attach some documentation to this particular usage and allow it to be easily recognized in application code.Nullable Controls and Reset
Careful observers may note that
null
is showing up in theFormControl
types above. This is because form models can be.reset()
at any time, and the value of areset()
control is by defaultnull
:This behavior is built into the forms runtime, and so the typed forms API infers nullable controls by default. However, this can make value types more inconvenient to work with. To improve the ergonomics, we're adding the ability for
FormControl
s to bereset
to a default value instead ofnull
:This gives you a choice – we’ll provide as much type safety as possible for old uses of
FormControl
, or you can provide a default value to get null-safety as well.FormGroup Types
A FormGroup infers a type based on its inner controls. Recall our
cat
type from above:In other words, a
FormGroup
's generic type is an interface that describes the types of each of its inner controls.This may seem surprising, as one might imagine this type should describe the values instead:
However, we want to strongly type not just
FormGroup.value
, butFormGroup.controls
. That is, the type ofcat.controls.name
should be the actual type of thename
control, and not a plainAbstractControl
type. This is only possible if the type ofcat
is built on the control types that it contains, not the value types of those controls.Disabled Controls
The value property of a
FormGroup
is an object that contains the values of each constituent control, with one important difference: the value key for every control is optional. That is, the type ofcat.value
in the example above looks like the interface:This may seem surprising - any given key on the value object may not be present (and thus
undefined
if read). This happens because of the way disabled controls work in aFormGroup
. When a control in a group isdisabled
, its value is not included in the value object:If you want a value object for the group that includes the values for disabled controls, use the
.rawValue()
method instead.The
get
MethodAbstractControl
provides aget
method for accessing descendant controls by name:We have implemented strong types for this method using template literal types. As long as a constant string literal is provided as the argument, we will tokenize it and extract the type of the requested control. If the argument is not a literal (e.g. it’s a
string
variable), the return type will beany
.Adding and Removing Controls
FormGroup
provides methods to dynamically modify its keys, such asremoveControl
. In this proposal, such a call will only be allowed if the key is explicitly marked optional:In this example,
lives
can be removed because theCatGroup
interface which describes theFormGroup
specifies it as an optional property. If the?
was not present in the type, then thelives
key would not be removable.Some applications use
FormGroup
as an open-ended dictionary, where the set of controls is not known at build time. For these cases, untyped forms can be used viaFormGroup<any>
.An alternative would be to introduce a new class,
FormRecord
, in which keys can be dynamically added and removed. The type guarantees forFormRecord
would be much weaker than with immutableFormGroup
.FormBuilder
In addition to typing the model classes, we have also added types to
FormBuilder
. Each builder method takes a type parameter, which will typically be inferred. That parameter works in the same manner as if the control had been constructed directly.There are a number of ways to provide values to FormBuilder. All of these methods have been strongly typed:
As you can see, you can provide a raw value, a control, or a boxed value, and the type will be inferred.
Usages of FormBuilder will have
<any>
or<any[]>
inserted on pre-existing method calls, to preserve backwards compatibility.Limitations
Control Bindings
When a
FormControl
is bound in a template, Angular's template type checking engine will not be able to assert that the value produced by the underlying control (described by itsControlValueAccessor
) is of the same type as theFormControl
. That is, the following:will result in
name.value
returning numeric values from the<input type="range">
, despite being typed asFormControl<string|null>
.This is a limitation of the current template type-checking mechanism, due to the fact that the
FormControlDirective
which binds the control does not have access to the type of theControlValueAccessor
which describes the DOM control - each directive type is independent of any other directives on a given element. We have a few ideas on how to remove this restriction, but feel there is significant value in delivering stronger typings for forms even without this checking in place.Template Driven Forms
The above restriction also applies to
NgModel
and template driven forms, which is why we've focused our efforts on reactive forms alone.Because reactive form models are created in TypeScript code, there's a natural syntax for explicitly declaring their types if necessary. No such syntax exists in Angular's template language, further complicating any potential typings for template driven forms.
Try the Prototype
There is a prototype PR with an implementation. Below, we provide two methods for trying it out. This is a draft implementation, with missing features and non-final design aspects.
To try it on StackBlitz:
ng serve
.profile.component.ts
to use the new typings.To try it with a demo app locally:
git clone https://github.com/dylhunn/typed-forms-example-app.git && cd typed-forms-example-app
npm i --force -g yarn && yarn
. As illustrated, you may need to force install them due to the experimental package versions.ng serve --open
src/app/profile/profile.component.ts
To try it with your app:
13.x.x
, upgrading if necessarygit checkout -b typed-forms-experiment
rm -rf node_modules
npm i
oryarn
next
release:ng update @angular/core --next
. Yourpackage.json
should now show that all angular packages are using the13.2.0-next.2
version or higher.package.json
in your project’s root directory. Find@angular/forms
, and replace~13.2.0-next.x
withhttps://1106843-24195339-gh.circle-artifacts.com/0/angular/forms-pr43834-8e5ba4f698.tgz
.npm i
oryarn
, depending on which package manager you are using). You will see peer dependency warnings because the experimental forms package has a prerelease version number; these should be ignored and/or overridden by force.git add . && git commit -m "upgraded to experimental angular package versions"
ng update @angular/core --migrate-only migration-v14-typed-forms
.any
s should have been inserted at all forms call sites. You can remove theseany
s to use the new types.Questions for Discussion
In addition to general feedback, we would like to collect feedback on the following specific questions:
1. Is there a compelling use case for tuple-typed FormArrays?
In the current design,
FormArrays
are homogeneous - every control in aFormArray
is of the same type. However, TypeScript itself supports arrays where each element has its own type - known as a tuple type.For example, the type
[string, number]
describes an array which must have at least 2 elements, where the first element is astring
and the second is anumber
.Our proposed design for
FormArray
does not support such cases (instead,FormArray<any>
could be used, falling back to untyped semantics).We are interested in any cases where a tuple-typed compound control would provide value.
2. Is there a compelling use case for a map style
FormGroup
?In the current design, a typed
FormGroup
requires that all possible control keys are known statically. In some applications,FormGroup
s are used as maps, with a set of controls with dynamic keys that are added at runtime. For these cases, we currently recommend falling back to untyped form semantics usingFormGroup<any>
.An alternative would be to provide an explicit
FormGroup
analogue that supports a dynamic mapping of controls. The tradeoff would likely be that all controls present in the grouping would have the same value type. Essentially, it would behave as the forms version of aMap<string, T>
.We would be interested in whether this kind of compound control ("
FormRecord
") would significantly improve the ergonomics of use cases whereFormGroup
is currently used to manage a dynamic set of controls.3. Is the current
null
ability ofFormControls
useful?The original forms API allowed for initialization at construction to a specific value. However, controls would always use
null
as a default value to which they would revert whenreset()
- this means that all controls would necessarily benull
able.For typed forms, we are introducing a configuration option to use the initial value as the default value instead, allowing for non-
null
able controls.Our long term plan is to remove the
null
reset behavior entirely, and always use the initial value as the default/reset value. To do this, in the future we will makeinitialValueIsDefault: true
the default behavior, and eventually deprecate and remove the flag entirely.For those cases where a truly independent initial value is required, the value can be changed via
setValue
immediately following the control's construction.We would be interested in any use cases where this change in default value behavior would be problematic or burdensome, and where the current
reset
-to-null
behavior is important.4. Are the limitations involving control bindings a blocking issue?
As discussed above, binding to controls via directives (such as
formControl
andformControlName
) is not type-safe. This can be improved in a future release, by adding warnings when a control is bound with an incompatible type. Is this shortcoming severe enough that we should delay any typings until it can be solved?5. Does the prototype migration correctly handle existing projects?
The prototype shown above includes a migration to add
<any>
to existing forms usages. We would be especially interested if any cases are discovered where this migration does not apply correctly or does not insulate existing code from the effects of adding types to forms.Beta Was this translation helpful? Give feedback.
All reactions