Angular: Reactive forms are not strongly typed

Created on 30 Dec 2016  ·  90Comments  ·  Source: angular/angular

[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.

forms feature high

Most helpful comment

Hey, I wanted to share an update from Angular team: we hear you that this is a big pain point. We will be starting work on more strongly typed forms soon, which will include looking at existing PRs and reviewing all your comments again. Thanks to all of you for taking the time to leave your thoughts!

All 90 comments

related #11279

This is not related to #11279.

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.

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.

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 (https://github.com/Microsoft/TypeScript/issues/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.

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.

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?

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

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

This might be of use - Angular Typesafe Reactive Forms

Small update on this:
As per my comment here: https://github.com/angular/angular/pull/16828#issuecomment-337034655
implementing generics into the current Forms API is not possible without breaking changes.
So either breaking changes are needed
Or a full forms rewrite

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

@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.

@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)

@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.

@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.

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

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.

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

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

+1. Can't count amount of instances when I wished it was implemented.

Same.
Angular Reactive forms is one the features that really beats any other framework out there. Making Reactive forms strongly typed would bring it to the next level, further widening the gap to the competition :)

this can be done with conditional mapped types and recursion..... conditional mapped types were just merged into typescript. If this will be published we have a chance to make strong typed forms possible

Can't wait(

@Quramy solution sadly does not enforce the structure of all arguments to FormBuilder. Also generic FormGroup<T>, FormControll<T> and FormArray<T> can not be used directly, because they are only interfaces which don't event extend AbtractControl<T>. This was not sufficient for our current project.

With ngx-strongly-typed-forms I haven now released a strong typed forms project myself
It breaks a bit with backwards compatibility, because it doesn't use default generics. So it forces you to explicity give your controls a type of any, but adds a lot more typesafety to all other parts and is API compatible with the current implementation of @angular/forms.
Perhaps this is a valid alternativ until this feature gets implemented in Angular.

+1 This is a powerful feature..

Should be implemented ASAP)

The way it's meant to be coded

There's an interesting implementation in this open source software (AGPL)

https://github.com/concentricsky/badgr-ui/blob/master/src/app/common/util/typed-forms.ts

I used this as a temporary workaround:
Hope it helps

import { FormControl, AbstractControl } from "@angular/forms";
export class NumberControl extends FormControl{
    _value: Number;
    get value(){ 
        return this._value;
    }
    set value(value){
        this._value = Number(value);
    }
}

This is something I've thought about a few times in projects I've worked on, but I haven't used JavaScript proxies enough to know the performance impact this would have on anything observing these values.

I simply created a custom workaround at the FormBuilder level:

import { Injectable } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
type ExtraProperties = { [key: string]: any } | null;
interface IModelConstructor { new(...args: any[]): any }

@Injectable({
    providedIn: 'root'
})
export class TypedFormBuilder {
    array = this.formBuilder.array;
    control = this.formBuilder.control;
    group = this.formBuilder.group;

    constructor(private formBuilder: FormBuilder) {}

    typedGroup<TGroupModel extends object>(ModelType: IModelConstructor, extraProps?: ExtraProperties): FormGroup & TGroupModel {
        const formGroup: any = this.group({
            ...new ModelType(),
            ...extraProps
        });

        return new Proxy<FormGroup & TGroupModel>(formGroup, {
            get: (target: FormGroup & TGroupModel, propName: string | number | symbol) => {
                if (propName in target) {
                    return target[propName];
                } else {
                    const property = target.get(propName as string);
                    return property && property.value;
                }
            },
            set: (target: FormGroup & TGroupModel, propName: string | number | symbol, value: any, _: any) => {
                if (propName in target) {
                    target[propName] = value;
                } else {
                    target.setValue({ [propName]: value });
                }

                return true;
            }
        });
    }
}

This solution is definitely not super polished, and it probably would be best to have the FormGroup generated access the values on a property (such as myGroup.fields, where "fields" would be the provided type). But this does provide strong typing. To use it:

generateFormGroup() {
    this.theGroup = builder.typedGroup<MyModel>(MyModel);
    this.theGroup.someProperty = 5;
}

I gathered experience in the last months with typed forms, by using my forms typings in my current project. It provides great value when working on one project with three developer teams and everybody switched because it was really cheap to do so.

But I want to discuss, if it's the right decision just to put generics on the current API. While building the types for all form controls, I found a lot of edge cases and things that are impossible or unwieldy to type, because I think static typing was not possible at that point in time and so not one of the biggest concerns.
Sadly this targets the main functionality with AbstractControl#value, which must be something like DeepPartial<T>, or AbstractControl#get with different implementations for each subclass.
Being backwards compatible also loses some of type safety caused by fall through cases with any types.
Perhaps considering a new API for reactive forms is also an option for this problem?

So, this is what I ended up doing while an actual solution happens.

Disclaimer... I just started in Angular, but quite familiar with Typescript, so I do not fully understand reactive forms... here is what ended up working for me, but of course it's not fully complete, I just typed FormGroup, but I'm sure more thing will need to be typed as I learn more about the forms...

import { FormGroup } from '@angular/forms';

export class FormGroupTyped<T> extends FormGroup {
  public get value(): T {
    return this.value;
  }
}

and then I can use it like this

import { FormGroupTyped } from 'path/to/your/form-group-typed.model';

interface IAuthForm {
  email: string;
  password: string;
}

const authForm: FormGroupTyped<IAuthForm> = fb.group({
  email: ['', [Validators.required]],
  password: ['', [Validators.required]],
});

const formValues: IAuthForm = this.authForm.value;
const email: string = formValues.email; 
const invalidKeyVar: string = formValues.badBoy; // [ts] Property 'badBoy' does not exist on type 'IAuthForm'. [2339]
const invalidTypeVar: number = formValues.password; // [ts] Type 'string' is not assignable to type 'number'. [2322]

@Toxicable Lol was searching this exact problem! haha

@cafesanu Here is a little improvement of your typed FormGroup to check the constructor.

import { AbstractControl, AbstractControlOptions, AsyncValidatorFn, FormGroup, ValidatorFn } from '@angular/forms';

export class FormGroupTyped<T> extends FormGroup {
  readonly value: T;
  readonly valueChanges: Observable<T>;

  constructor(controls: { [key in keyof T]: AbstractControl; },
              validatorOrOpts?: ValidatorFn | Array<ValidatorFn> | AbstractControlOptions | null,
              asyncValidator?: AsyncValidatorFn | Array<AsyncValidatorFn> | null) {
    super(controls, validatorOrOpts, asyncValidator);
  }

  patchValue(value: Partial<T> | T, options?: {
    onlySelf?: boolean;
    emitEvent?: boolean;
  }): void {
    super.patchValue(value, options);
  }

  get(path: Array<Extract<keyof T, string>> | Extract<keyof T, string>): AbstractControl | never {
    return super.get(path);
  }
}

usage :

export class SearchModel {
  criteria: string;
}

const searchForm = new FormGroupTyped<SearchModel >({
  criteria: new FormControl('', Validators.required) // OK
  badBoy: new FormControl('', Validators.required)  // TS2345: Object literal may only specify known properties, and 'badBoy' does not exist in type '{ criteria: AbstractControl; }'.
});

I wrote a little wrapper around FromControl that allows for dynamically generating the type of the data based on calls to a builder: https://github.com/concentricsky/badgr-ui/blob/develop/src/app/common/util/typed-forms.ts

The dynamically-constructed type ensure that the shape of the form matches your expectations, rather than having to pre-declare the interface type and then hope you make the right calls to create the form.

I'd love to see something similar to this in Angular at some point.

The usage looks like this:

// Create a typed form whose type is dynamically constructed by the builder calls
const group = new TypedFormGroup()
    .add("firstName", typedControl("", Validators.required))
    .add("lastName", typedControl("", Validators.required))
    .add(
        "address",
        typedGroup()
            .add("street", typedControl("2557 Kincaid"))
            .add("city", typedControl("Eugene"))
            .add("zip", typedControl("97405"))
    )
    .addArray(
        "items",
        typedGroup()
            .addControl("itemName", "")
            .addControl("itemId", 0)
    )
;

// All these are type checked:
group.value.address.street.trim();
group.controls.firstName.value;
group.untypedControls.firstName.value;
group.value.items[0].itemId;

Hi all in the last 3 days I was experimenting using a d.ts gist to define a more strit-ier definition for ReactiveForms classes creating new Typed interface compatible with original angular class.
I think that it may be a possible solution/workaround for your problem 😉

//BASIC TYPES DEFINED IN @angular/forms + rxjs/Observable
type FormGroup = import("@angular/forms").FormGroup;
type FormArray = import("@angular/forms").FormArray;
type FormControl = import("@angular/forms").FormControl;
type AbstractControl = import("@angular/forms").AbstractControl;
type Observable<T> = import("rxjs").Observable<T>;

type STATUS = "VALID" | "INVALID" | "PENDING" | "DISABLED"; //<- I don't know why Angular Team doesn't define it https://github.com/angular/angular/blob/7.2.7/packages/forms/src/model.ts#L15-L45)
type STATUSs = STATUS | string; //<- string is added only becouse Angular base class use string insted of union type https://github.com/angular/angular/blob/7.2.7/packages/forms/src/model.ts#L196)

//OVVERRIDE TYPES WITH STRICT TYPED INTERFACES + SOME TYPE TRICKS TO COMPOSE INTERFACE (https://github.com/Microsoft/TypeScript/issues/16936)
interface AbstractControlTyped<T> extends AbstractControl {
  // BASE PROPS AND METHODS COMMON TO ALL FormControl/FormGroup/FormArray
  readonly value: T;
  valueChanges: Observable<T>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}

interface FormControlTyped<T> extends FormControl {
  // COPIED FROM AbstractControlTyped<T> BECOUSE TS NOT SUPPORT MULPILE extends FormControl, AbstractControlTyped<T>
  readonly value: T;
  valueChanges: Observable<T>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}
interface FormGroupTyped<T> extends FormGroup {
  // PROPS AND METHODS SPECIFIC OF FormGroup
  //controls: { [P in keyof T | string]: AbstractControlTyped<P extends keyof T ? T[P] : any> };
  controls: { [P in keyof T]: AbstractControlTyped<T[P]> };
  registerControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): AbstractControlTyped<T[P]>;
  registerControl<V = any>(name: string, control: AbstractControlTyped<V>): AbstractControlTyped<V>;
  addControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): void;
  addControl<V = any>(name: string, control: AbstractControlTyped<V>): void;
  removeControl(name: keyof T): void;
  removeControl(name: string): void;
  setControl<P extends keyof T>(name: P, control: AbstractControlTyped<T[P]>): void;
  setControl<V = any>(name: string, control: AbstractControlTyped<V>): void;
  contains(name: keyof T): boolean;
  contains(name: string): boolean;
  get<P extends keyof T>(path: P): AbstractControlTyped<T[P]>;
  getRawValue(): T & { [disabledProp in string | number]: any };
  // COPIED FROM AbstractControlTyped<T> BECOUSE TS NOT SUPPORT MULPILE extends FormGroup, AbstractControlTyped<T>
  readonly value: T;
  valueChanges: Observable<T>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T> ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}

interface FormArrayTyped<T> extends FormArray {
  // PROPS AND METHODS SPECIFIC OF FormGroup
  controls: AbstractControlTyped<T>[];
  at(index: number): AbstractControlTyped<T>;
  push<V = T>(ctrl: AbstractControlTyped<V>): void;
  insert<V = T>(index: number, control: AbstractControlTyped<V>): void;
  setControl<V = T>(index: number, control: AbstractControlTyped<V>): void;
  getRawValue(): T[];
  // COPIED FROM AbstractControlTyped<T[]> BECOUSE TS NOT SUPPORT MULPILE extends FormArray, AbastractControlTyped<T[]>
  readonly value: T[];
  valueChanges: Observable<T[]>;
  readonly status: STATUSs;
  statusChanges: Observable<STATUS>;
  get<V = unknown>(path: Array<string | number> | string): AbstractControlTyped<V> | null;
  setValue<V>(value: V extends T[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  patchValue<V>(value: V extends Partial<T>[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
  reset<V>(value?: V extends Partial<T>[] ? V : never, options?: { onlySelf?: boolean; emitEvent?: boolean }): void;
}

Usage testing in stackblitz - please download code and run in local VSCode I don't know why on stackblitz the ERRORS for setValue and pathValue are not correct...

On this twitter thread I discuss with @IgorMinar of some "future ideas" (after V8+)

Any comments, suggestions and help are very welcome!

My solution called @ng-stack/forms. One of the interesting features is the automatic detect of form types.

So, no need to do this in your components:

get userName() {
  return this.formGroup.get('userName') as FormControl;
}

get addresses() {
  return this.formGroup.get('addresses') as FormGroup;
}

Now do this:

// Note here form model UserForm
formGroup: FormGroup<UserForm>;

get userName() {
  return this.formGroup.get('userName');
}

get addresses() {
  return this.formGroup.get('addresses');
}

For more info, see @ng-stack/forms

Hey, we've been working with @zakhenry recently on 2 things:

  • Improving form types
  • Improving the way to manage sub forms (within sub components)

We do not cover all the types as @dmorosinotto did but we've got some safety that'd definitely help when making refactors (for both ts and html :tada:).

If anyone wants to take a look to the lib: ngx-sub-form

Hi @maxime1992 I've looked at the Controls<T> but it doesn't strict the AbstractControl to <T[K]>
Can I ask you why? Do you leave it "untyped" because you feel the needs to change the type at run-time using something like setControl or registerControl methods to redefines and changes the FormControl and maybe the associated type?
Because my TypedForms.d.ts is a bit stricter and "force" the AbstractControlTyped to the type T<P> but I don't know if with this kind of choice enforce / disable something that in the original ReactiveForms API is allowed and maybe used by someone...

Any thought? Any real-case to consider?
Any comments on this may help me to decide on how to change the definitions that I've created and the PR that I'm working on...
Thanks

PS: Great work on ngx-sub-form 👍 the idea to use ControlValueAccessor to handle subforms is something I was experimenting too 😉

For @ng-stack/forms added support typed validation.

Classes FormControl, FormGroup, FormArray and all methods of FormBuilder
accept "error validation model" as second parameter for a generic:

class ValidationModel {
  someErrorCode: { returnedValue: 123 };
}
const control = new FormControl<string, ValidationModel>('some value');
control.getError('someErrorCode'); // OK
control.errors.someErrorCode; // OK
control.getError('notExistingErrorCode'); // Error: Argument of type '"notExistingErrorCode"' is not...
control.errors.notExistingErrorCode; // Error: Property 'notExistingErrorCode' does not exist...

By default used special type called ValidatorsModel.

const control = new FormControl('some value');
control.getError('required'); // OK
control.getError('email'); // OK
control.errors.required // OK
control.errors.email // OK
control.getError('notExistingErrorCode'); // Error: Argument of type '"notExistingErrorCode"' is not...
control.errors.notExistingErrorCode // Error: Property 'notExistingErrorCode' does not exist...

ValidatorsModel contains a list of properties extracted from typeof Validators, and expected returns types:

class ValidatorsModel {
  min: { min: { min: number; actual: number } };
  max: { max: { max: number; actual: number } };
  required: { required: true };
  requiredTrue: { required: true };
  email: { email: true };
  minLength: { minlength: { requiredLength: number; actualLength: number } };
  maxLength: { requiredLength: number; actualLength: number };
  pattern: { requiredPattern: string; actualValue: string };
}

@dmorosinotto Thank you so much but I am not able to use
Validators.compose([Validators.required, Validators.maxLength(20), Validators.minLength(3)])

Hi @youssefsharief can you give me more details of your problem with Validators? What kind of error/problem did you find using the .d.ts?

If you can post a sample code or a stackblitz I'll watch at it and I'll try to help solve the problem if I found a solution 😉

For @ng-stack/forms added support for input[type="file"].

See also example on stackblitz

I also used "classic" approach with using FormData to upload files, but there are several profits:

  • automatically retrieve form input name, e.g. <input type="file" name="someName"> -> formControl.value with someName field name;
  • support multiple attribute with properly setting field name (note userpic[] here)
<input type="file" multiple name="userpic" [formControl]="formControl">

```ts
formData.append('userpic[]', myFileInput.files[0]);
formData.append('userpic[]', myFileInput.files[1]);

- `Validators` have four static methods for files
```ts
import { Validators } from '@ng-stack/forms';

Validators.fileRequired;
Validators.fileMaxSize(1024);
Validators.filesMaxLength(10);
Validators.filesMinLength(2);

@KostyaTretyak from what I saw so far your package seems to be a great solution for the current typing issues in Angular Reactive Forms. Ever thought about contributing to Angular itself instead of doing your own implementation?

We are almost in angular 8 and still have no out-of-the-box typed forms in a 100% TypeScript environment which feels pretty odd to me.

@kroeder, I just see that the current issue was created more than two years ago (almost immediately after the release of Angular 2), and I see that similar Pull Request was created 1.5 years ago without a merge...

But if @ng-stack/forms will be popular and compatible with Angular 8+, maybe I will create the Pull Request in the future.

@KostyaTretyak Sounds great :) I've been using https://github.com/no0x9d/ngx-strongly-typed-forms which is mentioned earlier in this thread and created by @no0x9d How does your library differ from that?

@ZNS, I don't know, but after a quick review, I think there is no generic for validation in ngx-strongly-typed-forms, as well as the Automatically detect appropriate types for form controls. Maybe I'm wrong.

@ZNS @KostyaTretyak Hi there. As mentioned above, I am the author of ngx-strongly-typed-forms.
I did a quick review of the feature set of @ng-stack/forms and think there are some small differences.

There is a conceptional difference between nearly all solutions or workarounds and my project.
In most cases the original Angular FormControl is extended or wraped by some Proxy. Some methods are overridden with other type signatures and delegated to the original function.
This introduces a new layer with a negliable performance impact, but more important is code that has to be maintained and can introduce bugs to your project.
In my opinion this is unnecessary for static checks at compile time. The only run time part is the NgModule that provides my typed FormBuilder, which is in fact the original Angular FormBuilder. Everything else is just Angular code.

In direct comparison I don't have the ValidationModel and conversion from objects to FormGroups and Arrays to FormArrays but did some opinionated changes to AbstractControl#get to make it more type safe and have typed validator arguments.

With some small additions my code can be backwards compatible and I could create a pull request. But a simmilar pull request is stale for a long time.
But if there are efforts to get this in Angular I would be happy to join forces. On the other side I would love to see a new API for forms that is better designed to be strictly typed. See my comment for details.

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

bump to angular team?

They don't care about developers. They have their own backlog, with blackjack and awesome-5%-decrease-bundle-size features.

@Lonli-Lokli

They don't care about developers.

Criticizing is easy. Do you care more about other devs?
Haven't seen a PR from you to improve forms nor any kind of constructive comment or RFC to make things move forward :man_shrugging:

They have their own backlog

No way :fearful:!
People are prioritizing things that the company _[who's paying them]_ needs?
What a shame!
image

awesome-5%-decrease-bundle-size features.

You're clearly talking about Ivy and the (currently) very small difference in bundle size.
Ivy is currently experimental and you've got to opt in. What a surprise things aren't perfect yet! :thinking:
Yes, it's been told that Ivy will help reduce bundle size and allow tools to do a better tree shaking on the apps. And hopefully, that will come! For now they're only working on it making sure that it's not breaking anything and can later help to have better debugging info, incremental compilation on a component basis rather than module basis, and tree shaking. But tooling to get that tree shaking will work later.

So please try to be respectful and avoid trashing people that are giving you an open source framework for free. Things aren't perfect, huge work under way, yes it feels like some issues are left behind but that refactor was needed and there will never be any good time to make something as big as that, it just had to happen at some point.

Now I'm out of this debate because I don't want to monopolize this thread talking about non productive things. *flies away*

@maxime-allex
There are a lot of other PRs (386 as of now), do you think one more will change anything?
Speaking of this issue, this related PR (https://github.com/angular/angular/pull/20040) still not merged.

Speaking of refactoring, Ivy was mentioned year ago. I know that somebody can treat is a major feature for developers, but personally me prefer to see fixes are important for as many developers as possible.
https://github.com/angular/angular/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc

I still hope that Angular is for developers, not for the marketing, and hope to see more issues closed based on community' reaction. That is my question, what are the priorities here.

Obviously this issue can be addressed via an update to the existing API, but, separately, I've created a proposal to improve the ReactiveFormsModule to address a number of outstanding issues with it, including this one.

Some other issues addressed include the ability subscribe to updates on any property and the ability to asynchronously validate a control via a service (rather than an asyncValidator). You can learn more over in #31963. Feedback is welcomed!

As I promised before, I created Pull Request. This PR contains only part of the @ng-stack/forms feature (without: validation, auto detect form control and support input[file]).

Hey, I wanted to share an update from Angular team: we hear you that this is a big pain point. We will be starting work on more strongly typed forms soon, which will include looking at existing PRs and reviewing all your comments again. Thanks to all of you for taking the time to leave your thoughts!

Oh!!!!! Exiting!

That is very good news, Angular team, thanx!
As soon as there is a release, I'll deprecate angular-typesafe-reactive-forms-helper.

YESSSS!!!!

I’m so excited!! Thank you, Angular team!!

Can we stop with the reaction spam?
Use the emojis for reactions, as they're intended for https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/ - thanks.

As Angular team has confirmed to work on strongly typed Reactive Form. I would like to share my reasons of my implementation that heavily use infer type to extract value type in order to get a smooth static typed development experience.

1st Approach: Started with Value Type

When I started designing the FormGroup, I used a intuitive simple value type as T.

FormGroup<{ id: string, username: string | null }>

But I found that when I have to handle complex table form architecture, it is very difficult to do a type-safe binding in Angular HTML.

type User = { id: string, username: string | null }
const table: FormArray<User> = new FormArray([{ id: '0', username: 'gaplo917'}])

const row: FormGroup<User> = table.at(0) as FormGroup<User> // you must do a type cast

// NO way to get type-safe binding in Angular HTML
<input [formControl]="table.at(i).controls.id" />

The above implementation required developers to do custom type cast which is tedious and error-prone. IMHO, this completely lose the reason to use strongly typed Reactive Form.

2nd Approach: Started with a ControlType

As using a simple value type is not working smoothly. I come up another idea to use a KeyValueControl as T and use infer the extract the value type from the KeyValueControl recursively.

export type KeyValueControl<V> = {
  [key in keyof V]: V[key] & AbstractControl
}

export type InferTypedForm<Type> = Type extends TypedFormControl<infer X>
  ? X
  : Type extends Array<TypedFormControl<infer X1>>
  ? X1[]
  : Type extends Array<TypedFormGroup<infer X2>>
  ? Array<InferTypedFormGroup<X2>>
  : Type extends TypedFormGroup<infer X3>
  ? InferTypedFormGroup<X3>
  : Type extends TypedFormArray<infer X4>
  ? Array<InferTypedForm<X4>>
  : never

export type InferTypedFormGroup<T> = { [key in keyof T]: InferTypedForm<T[key]> }

export type InferTypedFormArray<T> = InferTypedForm<T[]>

export class TypedFormGroup<T extends KeyValueControl<T>> extends FormGroup {
  readonly controls: T
  readonly valueChanges: Observable<InferTypedFormGroup<T>>
  readonly statusChanges: Observable<FormStatus>
  readonly value: InferTypedFormGroup<T>
  ...
}

As a result,

interface Foo {
  first: TypedFormControl<string | null>
  last: TypedFormControl<string | null>
}

const table: FormArray<FormGroup<Foo>>

const row: FormGroup<Foo> = table.at(0) // typescript compile OK!

// type-safe binding in Angular HTML, all auto-complete (`id`, `username`) finally works!
<input [formControl]="table.at(0).controls.id" />

Live Demo

Edit gaplo917/angular-typed-form-codesandbox

Live Demo IDE Screenshots

That is a true auto complete experience in a complex forms.
Screenshot 2020-06-12 at 19 02 10

Angular Typed Forms

100% Compatible to existing Reactive Form Modules
https://github.com/gaplo917/angular-typed-forms

Comments and improvements are welcome. Hope that the upcoming Strongly Typed Reactive Forms can handle complex form models like Table and Nested Sub-Forms.

@IgorMinar , Angular Core Team, and Angular Community Members.

This long comment is fully focused on two statements which are highlighted by ticket author "against the practices" and "strongly typed".

I would suggest instead of an interface-based approach for Strongly Typed Reactive Form we should opt Class-Based Approach but not in the way that's mentioned in the ng-stacks/forms. Also would not recommend to change the code base of Angular Ractive Forms, Because we can achieve a strongly typed form without changing the codebase through many ways. Let me describe in detail what are the high-level challenges I see in the Interface Driven Approach and Class Driven Approach is more Intuitive than others and also we are getting the FormGroup object is Strongly-Typed. In both cases, our FormGroup object is Strongly Typed we are not losing the power of TypeScript Type in Class-Based Approach.

My Suggestion

As we all are familiar with OOP practices, the class gives us more flexibility and maintainability of the code. few of the benefits I am highlighting below:

  1. Decouple our code.
  2. Less Code as compared to the current approach as well as interface-driven approach.
  3. We can use Custom Decorators on the property.
  4. The code is readable, maintainable, and extensible.
  5. With this approach, we can don't need to write the business logic in the template, like we are putting the *ngIf with multiple conditions for showing the error messages. I believe the templates are not meant for writing the business logic.
  6. Lot more...

Let me transformed the above-mentioned Interface code into the Class and apply the validation decorator on the Class properties. Here we are following the Single Responsibility Principle Practices. Have a look at the below code:

image

Let's consider a few cases and compare it with the Interface and Class Driven Strongly-Typed Approach that helps us to understand the difference in both of them.

1. Create a FormGroup
Here we are using the same instance of FormBuilder and the same method of group. But the import module name will be different like ReactiveTypedFormsModule instead of ReactiveFormsModule. Let's create a FormGroup:
image

As per the above code, the question comes,

Is the current approach will work after importing the ReactiveTypedFormsModule?
Yes, It will work, nothing will be changed after importing the ReactiveTypedFormsModule.

Let's quickly see the other cases and conclude this post.

2. Change the Value of FormControl
Instead of calling setValue method, we can directly assign the value on the Class property. It will automatically set the FormControl value.

image

3. Perform business logic based upon value changes of the FormControl
Instead of Subscribing the FormControl ValueChanges, use the power of setter method in TypeScript.

image

4. Convert the Input Value
We are focusing on Strongly-Typed but what about those values which are coming from the input control like for date we are getting the value in String format but we are expecting Date Format in TS code, to overcome this problem we create a directive or method to converts the value as required. I won't show here the current code as that's a bit clumsy because we have to create the directive and do the blah blah things.... 😄, So I would like to show Class Driven Approach code here:

@toDate() // this will convert the value before assigning the value in 'dob' property.
dob:Date;

4. Change the Value of Nested FormGroup FormControl
We can directly assign the value in the respective property rather than getting the Nested FormGroup Object and call the SetValue method.

image

5. Adding FormGroup in Nested FormArray
No more to say, have a look at the below code 😄 .

image

Reference the FormGroup Object in HTML Template

Simple Code 😄 . Nothing will be changed in the HTML template, but you will get more in HTML template as well. Please refer to the below code

<form [formGroup]="user.formGroup">
   <input type="text" formControlName="firstName"/>
   <small >{{user.formGroup.controls.firstName.errorMessage}}</small>
<!--Nested FormGroup-->
   <div [formGroup]="user.address.formGroup">...</div>
<!--Nested FormArray - Skills-->
   <div *ngFor="let skill of user.skills">
       <div [formGroup]="skill.formGroup">...</div>
   </div
</form>

Stackblitz Link: Strongly Type Reactive Form Working Example
Example on Github: Strongly Typed Reactive Form

@ajayojha, correct me if I'm wrong, but it seems that your comment above can be reduced to this:

  1. Overhead that goes with TypeScript types is bad.
  2. Runtime validation with the help of decorators - it's good.
  3. Why do you need setValue() and valueChanges() if there are setters/getters?

What do I think:

  1. Writing TypeScript types is like writing static tests. Somebody may create applications without tests because they think it's unnecessary, but it's a bad practice.
  2. Runtime validation with the help of decorators - it's can be a good idea, agree.
  3. In addition to setValue() there are also patchValue(), reset(), which also work with form value. Replacing only setValue() with a setter will make the code inconsistent. In addition, when we have to write setters for each form model property, it will add much more code overhead as well as performance overhead in case with getters. Mixing form model properties names with form control properties is also a bad idea in my opinion.

Thanks, @KostyaTretyak for your concerns on my comment, I would be happy to answer the same, please see below my comments accordingly :).

  1. Overhead that goes with TypeScript types is bad.

Just for your information the formgroup object is strongly typed. Interfaces are good but not suitable for every area and I am not saying TypeScript types are bad, I don't think somewhere I have mentioned that the types are bad. My only concern is on Interface because we are overruling the software design practices with the Interface approach or even I can say we are using the Interface approach at the wrong place and my suggested approach is Class. As far as my understanding through the Class approach we are not compromising the benefit of TypeScript Types which we get in Interface or even I would say we get more than Interface approach in terms of Readability, Scalability, and Maintainability.

Are we using the right practice of Interface for Strongly Typed Reactive Form?

Let me describe a little bit more in terms of Interface is the bad practice(as per me) for Strongly Typed Reactive Form.

TypeScript Types are good, but it's not suggested that everywhere we have to jumble up anything which is not according to the software practices Like I have clearly mentioned the problems with the Interface. Just to think about my highlighted concerns on Interface. Let me share my case, In one of my enterprise application which contains more than 6k+ components. If I am going with Interface approach then the development team will ask me good questions before doing the changes:

  • Which interface we have to use where, as the Same Entity is used in multiple Components with different properties. Then Do we need to create the Separate Interface Component wise? If Yes, Then read the second case.
  • If we go with the Above approach then what will be the approach where I have to use both interface properties in one or many components. For the solution to this concern, I can create one more interface and extend the same. Is this good to create more and more files just for the shake of Strongly Type Reactive Form? What about the maintainability?, I don't have the answer, except to say that the Angular Team is providing this solution so it's good :) (if Angular Team will opt the Interface Approach).
  • If we go with a+b approach then in some of my components required few properties not all, then? I have three solutions to give my development team.

    • Create a new interface and copy/ paste the required properties in the newly created interface. This is the dirtiest approach in the Software World. This creates a lot of problems when any single property will be changed on the server side then it's difficult to track the areas alike in how many interfaces to change the property name.

    • Set the property nullable. If I am defining the property nullable then Why I have to follow the 'B' approach?. Again I don't have an answer :( to give my development team.

    • Don't create another Interface, Use 'Partial' Utility Type, and make every property optional. By doing this we are losing the actual benefit of interface. This is also against the practices. If I have to follow this then Why I have to follow the 'A' approach, again No answer :).

    • If I am making every/few properties nullable then what about the code readability and how can I judge how many properties are required for passing the value to the server. Then I have to check in the respective component and get a glimpse. Major code readability concern.

Now Just to think over it the above cases on a larger perspective and compare with TypeScript Types with Interface on Reactive Forms for Strongly Typed. I believe every good approach will save development time and in this approach Sorry to say I don't see any benefit according to the Software Design Principle and Practices.

  1. Runtime validation with the help of decorators - it's good.

I agree with your comment on "It's good", The decorator approach we cannot achieve in Interface Approach. I believe this is the most powerful feature of TypeScript, then why we cannot use the same in Reactive Form Approach and give the development team to full control of their object properties.

  1. Why do you need setValue() if there are setters?

Where I have said that I need 'setValue()'? I don't need setValue and I haven't shown in the example where I am calling the setValue method in Class Driven Approach. Please correct me if I am wrong.

  1. Writing TypeScript types is like writing static tests. Somebody may create applications without tests because they think it's unnecessary, but it's a bad practice.

I am not saying the TypeScript types is like writing static tests. But I don't agree with the commit changes in the base classes of reactive form, I believe we can achieve the same thing without touching the class definition. Here, we can use the actual power of interface which we are not using as per the commits so far, Is this is a good practice that the logic is running for so long and we are adding the generic types by setting the default value of 'any'?
I think that we can achieve the same thing without touching the base classes of Reactive Form. I don't know why we are not taking advantage of Interface in this instead of changing the base class definition and also changing the spec.

  1. Runtime validation with the help of decorators - it's can be a good idea, agree.

Good to know that We both are the same page on this :).

  1. In addition to setValue() there are also patchValue(), reset(), which also work with form value. Replacing only setValue() with a setter will make the code inconsistent. In addition, when we have to write setters for each form model property, it will add much more code overhead as well as performance overhead. Mixing form model properties names with form control properties is also a bad idea in my opinion.

Let me describe the above point in three sections calling method, setter performance overhead, and mixing form model properties.

Calling Method: As expected, while writing this post I was thinking that someone may suggest me use the 'patchValue' or 'reset' method. Again I would like to say in the real world case mostly development team is using the 'setValue' method instead of patchValue or other methods (This is my experience according to the Angular Application Code Review and Stackoverflow Posts setValue vs patchValue). My focus is just calling the method for assigning the value, no matter which method we are calling for.

Setter Performance: I agree with the statement of setters creates a performance overhead. If this is the case then I would say we have to fix first in Angular Project because for binding the reactive form, Angular Framework is using the setter method in Control Value Accessor class and so many other directives and this creates a performance overhead without using the Class Approach. One more thing the same approach we are also using in multiple components with @Input decorator, we have to find the alternate or Angular team have to provide a different solution(I believe) to overcome this kind of performance issue. So, I don't think this is a major concern. Now coming to the performance concern, Please compare with the existing approach and setter method approach(this is optional, the development team can opt if they wish like the same as ChangeDetectionStrategy in Angular. Please refer to the example on the rxweb documentation site for opting this case. Judge that how many function are calling when we subscribe the value changes then after set the value or directly calling the setter method. I believe this is much more intuitive in terms of less code execution as compare to valuechanges, low size of build package, code readability and so many other good things.

Mixing the Properties: So what's your opinion, are you assigning a different FormControl property name than the server returned property name. If Yes, then I would say this is a major problem in the code because every time I have to change the property name before posting it to the server, Sorry but I would not prefer in throughout the application. If I consider your good opinion for my application form which contains average 40+ fields then I have to set every property value manually, Just to think about the code in the component just for the shake of assigning the value and the prod build size. Is this is better opinion than the class approach?
Now come to the proposed solution, we are not mixing two things in one. FormControl properties are different and class properties are different from the respective data type. If you wish to change the property name like FormControl property name is different than the Data property then you can, please refer the rxweb reactive form package documentation. So there is no problem as your feeling of bad(mixing the property name with form-control names) has a solution in the proposed approach.

I hope I have answered all of your concerns, Feel free to share if you any other concerns on this :).

As I said in my previous comment that there is no need to change in the base classes of Reactive Form because we can achieve the same things by using the power of Interface Segregation Principle Practices. Here is the End-to-End Strongly-Typed Reactive Form solution with the package of @rxweb/types. This works well with Interface as well as Class approach :).

How Code Looks Like After Implementation?

Stackblitz: Open
Github: Interface Driven Strongly-Typed Reactive Form Example

Someone have any suggestion feel free to share the same.

So, Version 10 of Angular Now Available, this is a major release and apparently reactive forms will not be strongly typed until at least version 11 of the Angular. So, we will have to wait at least until the fall to implement this feature.

I have a question (or a proposal?) regarding the way the form model is built in most of the suggestions/PRs I saw here.

When looking at most libraries and PRs that try to make the Reactive Forms type safe, you can see that they create a model that looks like this:

interface Address {
  name: Name;  
}
interface Name {
  firstName: string;
  lastName: string;
}

this is then "translated" to someting like this:

const myForm = new FormGroup<Address>({
  name: new FormGroup<Name>({
    firstName: new FormControl('John'),
    lastName: new FormControl('Doe'),
  })
})

So, in simplified words: "If it is an object, then build a FormGroup for it. If it is an array, then build a FormArray. And if it is a primitive value, then create a FormControl."

But, there is one issue: You cannot use objects in FormControls anymore.

The solutions I saw so far: Some libraries simply don't support this. And some libraries use some sort of "hack" to create a hint that you actually want to use a FormControl instead of a FormGroup.

My question/proposal: What would speak against explicitly defining the form model as follows?

interface Address {
  name: FormGroup<Name>;  
}
interface Name {
  firstName: FormControl<string>;
  lastName: FormControl<string>;
}

const myForm = new FormGroup<Address>({
  name: new FormGroup<Name>({
    firstName: new FormControl('John'),
    lastName: new FormControl('Doe'),
  })
})

This has the huge advantage that you can now put objects into FormControls. And it doesn't require any kind of "hack" to do so :)

I have created a Codesandbox for this, so that you can maybe try it out yourself: https://codesandbox.io/s/falling-grass-k4u50?file=/src/app/app.component.ts

@MBuchalik, yes, this is the first obvious decision that comes to mind when you start working on "strong typed forms". I also started with this, but it has a significant disadvantage - the need to create two models: one for form controls, the other - for form values.

On the other hand, as far as I understand, this solution will allow us to implement "strong typed forms" without breaking chage, and we won't have to wait for the release of the next major version of Angular. Here it is necessary to work in practice with such solution to assess whether it has more critical shortcomings than the need to create two models.

@MBuchalik I shared the same opinions with you and have raised the same question to the PR and one of the angular contributors(@KostyaTretyak ) has answered.

You might take a look of the discussion in the PR:
https://github.com/angular/angular/pull/37389#discussion_r438543624

TLDR;

Here are a couple of problems:
If we follow your approach, we need to create two different models - for form controls and for form values.
The model for form controls is harder to read.

And I have implemented this idea for our projects half year ago that already be used in the production. A FULL static auto-complete experience for the controls type in Angular HTML really boost our junior developer productivity. (with fullTemplateTypeCheck enabled)

I have shared "why I go this way" in previous comment in this thread:
https://github.com/angular/angular/issues/13721#issuecomment-643214540

Codesanbox Demo: https://codesandbox.io/s/github/gaplo917/angular-typed-form-codesandbox/tree/master/?fontsize=14&hidenavigation=1&theme=dark

Thanks for your insights @KostyaTretyak and @gaplo917! 👍

If I understood it correctly, we can summarize it as follows.

If we only want to use one single model, then a solution like the one provided by @KostyaTretyak could be used. The downside however is that we now cannot use objects in FormControls anymore. (I know that there is a "hack" which would allow this. But then our model is again not "clean"; so we would once again need 2 models.)

If we want to be able to use objects in FormControls, then there is probably (!) no way around using an approach like the one I (or @gaplo917) illustrated. The downside is that you basically need 2 models. Or at least use some helper types to "extract" the form value model.

So, we now just need to think about whether objects in FormControls should be possible or not. This would simply answer the question regarding which of the two approaches is the one to select. Or am I missing something?

Thanks for your insights @KostyaTretyak and @gaplo917! 👍

If I understood it correctly, we can summarize it as follows.

If we only want to use one single model, then a solution like the one provided by @KostyaTretyak could be used. The downside however is that we now cannot use objects in FormControls anymore. (I know that there is a "hack" which would allow this. But then our model is again not "clean"; so we would once again need 2 models.)

If we want to be able to use objects in FormControls, then there is probably (!) no way around using an approach like the one I (or @gaplo917) illustrated. The downside is that you basically need 2 models. Or at least use some helper types to "extract" the form value model.

So, we now just need to think about whether objects in FormControls should be possible or not. This would simply answer the question regarding which of the two approaches is the one to select. Or am I missing something?

@MBuchalik In my opinion, if you trust Typescript compiler and heavily rely on the "type inference" feature, you don't need to have 2 models. Our internal system has 60+ forms, some of them are very complex nested with 3 depth level FormArray-FormGroup-FormArray and we also don't need explicit model for the value type.

There are only 2 type of data models to play with which are:

  • API Data Request/Response Model
  • FormControl Model

99.9% of time, we

  1. Create an encapsulation for each complex form
  2. Transform the remote data -> form data
  3. Transform the form data -> remote payload

the following code snippet is the illustration:

interface FooApiData {
   id: string
   age: number
   dob: string | null
   createdAt: string
}

interface FooFormControlType {
  id: TypedFormControl<string>
  age: TypedFormControl<number>

  // calendar view required JS date form control binding
  dob: TypedFormControl<Date | null>
} 

interface FooApiUpdateRequest {
   id: string
   dob: string | null
   age: number
}

class FooForm extends TypedFormGroup<FooFormControlType> {
    constructor(private fb: TypedFormBuilder, private initialValue: FooApiData) {
      super({
          id: fb.control(initialValue.id, Validators.required),
          dob: fb.control(initialValue.dob === null ? new Date(initialValue.dob) : null),
          age: fb.number(initialValue.age, Validators.required)
       })
   }
   toRequestBody(): FooApiUpdateRequest {
       const typedValue = this.value
       return {
          id: typedValue.id,
          dob: typedValue.dob !== null ? moment(typedValue.dob).format('YYYYMMDD') : null,
          age: typedValue.age
       }
   }
}

const apiData = apiService.getFoo()

const form = new FooForm(new TypedFormBuilder(), apiData)

// assume some UI changes the form value
function submit() {
   if(form.dirty && form.valid){
     const payload = form.toRequestBody()
     apiService.updateFoo(payload)
   }
}

P.S. This is an opinionated data flow architecture that we can enjoy programming type-safely in Typescript

If we only want to use one single model, then a solution like the one provided by @KostyaTretyak could be used. The downside however is that we now cannot use objects in FormControls anymore. (I know that there is a "hack" which would allow this. But then our model is again not "clean"; so we would once again need 2 models.)

Here it is still necessary to estimate on how often we need to use an object for FormControl. I think it can be estimated somewhere at 5-30%. That is, if we use solutions with one model, we can cover 70-95% of cases of using FormControl. For the rest - just provide a hint for TypeScript as an additional type (see Control<T>, it is not correct to call it a "second model"):

interface FormModel {
  date: Control<Date>;
}

Can the Control<T> type be called a hack? - Yes, it's probably a hack, but not a rough hack. I do not know of any cases where this type does not work as intended, or has side effects.

Oh, I remembered the side effects with Control<T> when we need to use external library for form value model. In such cases, two models are really needed:

import { FormBuilder, Control } from '@ng-stack/forms';

// External Form Model
interface ExternalPerson {
  id: number;
  name: string;
  birthDate: Date;
}

const formConfig: ExternalPerson = {
  id: 123,
  name: 'John Smith',
  birthDate: new Date(1977, 6, 30),
};

interface Person extends ExternalPerson {
  birthDate: Control<Date>;
}


const fb = new FormBuilder();
const form = fb.group<Person>(formConfig); // `Control<Date>` type is compatible with `Date` type.

const birthDate: Date = form.value.birthDate; // `Control<Date>` type is compatible with `Date` type.

But in this code, the overhead is only here:

interface Person extends ExternalPerson {
  birthDate: Control<Date>;
}

Thanks to @ArielGueta, a critical issue with the Control<T> type has now become known. That is, I won't even try to implement Control<T> in future Pull Requests for Angular, as I planned before.

Thanks to @ArielGueta, a critical issue with the Control<T> type has now become known. That is, I won't even try to implement Control<T> in future Pull Requests for Angular, as I planned before.

@KostyaTretyak It is not true. The critical issue only shows that your "ControlType" implementation is incorrect.

A full "Control Type" implementation has no problem.

Live Demo: https://codesandbox.io/s/lucid-bassi-ceo6t?file=/src/app/demo/forms/type-test.ts

Screenshot 2020-07-01 at 00 35 11

That is, I won't even try to implement Control in future Pull Requests for Angular, as I planned before.

OK, so that means that your PR (and consecutive ones) will most likely never support objects in FormControls?

@MBuchalik, at the moment (Angular v10), if we have the following form model:

interface FormModel {
  date: Date;
}

and if in our component we want to access the value of the date property, we need to do the following:

get date() {
  return this.formGroup.get('date') as FormControl;
}
// ...
this.date.value as Date;

My current pull request provides a generic for the form value, but does not provide a type for form control:

get date() {
  return this.formGroup.get('date') as FormControl<Date>;
}
// ...
this.date.value; // Here Date type

@gaplo917, @MBuchalik, I've tried your solutions, and tried to implement my own similar solution, but they all do not work perfectly. This solutions also provides a set of types for recursively extracting form model values. Overhead and breaking changes are very significant, see PR draft.

I strongly doubt that at the moment these solutions should be proposed to be implemented in Angular. That is, for now we will have to use generics only for form values, not for form control types.

but they all do not work perfectly

I have only spent a few hours on my illustration, so I did not expect it to be perfect ;) Could you give examples on things that don't work well? (Especially on stuff that, from your point of view, cannot easily be fixed?)

Btw, one suggestion regarding backwards compatibilty: From my point of view, it is relatively hard to make the implementation completely backwards compatible. Because of this, we could maybe do the following: We don't change the FormControl, FormGroup and FormArray classes at all. Instead, we create new ones that inherit from them (maybe call them StrictFormControl<T> and StrictFormGroup<T> or whatever you like). These are then the ones we make type safe. The benefit: We are 100% sure that no breaking change is done. :)

few hours on my illustration, so I did not expect it to be perfect ;)

I worked with this solution for a couple of days, and I see how difficult it will be to work with forms.

  1. First of all, significant overhead and the need to have two models.
  2. Secondly, this solution is no better in terms of reliability than my solution with Control<T> type, because in the same way it is necessary to recursively extract the value of form model.
  3. Work with nested form controls. If we have the following form model:
interface FormModel {
  one: FormGroup<{two: FormControl<string>}>;
}

And if we get formGroup.controls.one.value, TypeScript provide hint with conditional type, not with {two: string} type (as it should be). So value hard to read from IDE.

And if we get formGroup.controls.one.value, TypeScript provide hint with conditional type, not with {two: string} type (as it should be). So value hard to read from IDE.

OK so just to make sure I understood everything correctly. If you use my implementation and write the following:

interface FormModel {
  one: FormGroup<Two>;
}
interface Two {
  two: FormControl<string>;
}

const myForm = new FormGroup<FormModel>({
  one: new FormGroup<Two>({
    two: new FormControl('')
  })
});

(made it a bit more verbose ;) )

If I now look for myForm.controls.one.value then it looks like this:

grafik

So you say that, at this example, "two" should not be optional? I guess this is not the right way to type the form value. The form value only includes not disabled fields. So, from my point of view, it should be a recursive Partial. You can't know on compile time which fields will be disabled and which won't.

So you say that, at this example, "two" should not be optional?

What? No.

My test of your solution:

interface FormModel {
  one: FormGroup<{two: FormControl<string>}>;
}

let formGroup: FormGroup<FormModel>;
const some = formGroup.controls.one.value;

What I see on codesandbox after hover mouse on value:

(property) FormGroup<FormGroupControls<{ two: FormControl<string>; }>>.value: PartialFormGroupValue<FormGroupControls<{
    two: FormControl<string>;
}>>

Here PartialFormGroupValue refers to the conditional type PartialFormValue.

Ah, ok, I think I got it. So you mean that the type is hard to read, right? I originally thought you were talking about a bug or someting like that.

Well, most IDEs will still just present the suggestions for the available properties once you continue typing. So I don't really see any huge problems here. (Of course, it would be better to read if it just said {two?: string}. But I don't think this is super important. That's at least my opionion.)

If you implemented your Control<T>, how would you then remove it from the typing of the form value without doing sth like I did? And how would you make the form value a recursive partial without using a helper type?

If you implemented your Control, how would you then remove it from the typing of the form value without doing sth like I did? And how would you make the form value a recursive partial without using a helper type?

In this case, my solution is not better:

(property) FormGroup<FormGroup<{ two: FormControl<string>; }>>.value: ExtractGroupValue<FormGroup<{
    two: FormControl<string>;
}>>

I gave this example because you asked for it:

Could you give examples on things that don't work well? (Especially on stuff that, from your point of view, cannot easily be fixed?)

By the way, I fixed critical issue with Control<T>.

To resolve the HTML binding issues with Angular 10 and [formControl], this is the route I went:

As noted in another issue (https://github.com/angular/angular/issues/36405#issuecomment-655110082), for my forms I generally create classes that extend FormGroup to ease in re-usability and testing. With that structure, I was able to resolve the problem for now by updating my code from something like this:

class UserFormGroup extends FormGroup {
  constructor() {
    super({
      id: new FormControl(null, Validators.required),
      name: new FormControl(null, Validators.required),
    });
}

To this:

// everything will extend these two
export class EnhancedFormGroup<T extends { [key: string]: AbstractControl }> extends FormGroup
{
  controls!: T;
}
export class EnhancedFormArray<T extends AbstractControl> extends FormArray 
{
  controls!: T[];
}

// reworked form from above
function formDefinition() {
   return {
      id: new FormControl(null, Validators.required),
      name: new FormControl(null, Validators.required),
    };
}

class UserFormGroup extends EnhancedFormGroup<ReturnType<typeof formDefinition>> {
  constructor() {
    super(formDefinition());
}

At that point form.controls will correctly show its type as { id: FormControl, name: FormControl }, binding correctly in the HTML, and would correctly aggregate down if the form was more complicated with nested formgroups or arrays.

Using the formDefinition function isn't pretty, but it was the cleanest solution I could come up with to prevent duplication between the form definition and the constructor.

I believe you could update FormGroup to have the above generic type definition without introducing breaking changes (well, that may not be true for forms that dynamically add/remove controls I guess; they wouldn't show in the controls type)

edit
It looks like it's even simpler if you don't need to create classes that extend FormGroup; you could create a helper function that resolves the generic issue:

function createEnhancedFormGroup<T extends { [key: string]: AbstractControl }>(controls: T) {
  return new EnhancedFormGroup<T>(controls);
}

const form = createEnhancedFormGroup({
  id: new FormControl(null, Validators.required),
  name: new FormControl(null, Validators.required),
});

edit 2
... or you could bake it into the FormGroup class itself (FormBuilder maybe?)

export class EnhancedFormGroup<T extends { [key: string]: AbstractControl }> extends FormGroup
{
  controls!: T;

  static create<T extends { [key: string]: AbstractControl }>(controls: T) {
    return new EnhancedFormGroup<T>(controls);
  }
}

const form = EnhancedFormGroup.create({
  id: new FormControl(null, Validators.required),
  name: new FormControl(null, Validators.required),
});

edit 3
I've extended the above examples to include typing on the values and created an article to sum everything up:

https://medium.com/youngers-consulting/angular-typed-reactive-forms-22842eb8a181

This is now marked on the roadmap for Future development: https://angular.io/guide/roadmap#better-developer-ergonomics-with-strict-typing-for-angularforms

@pauldraper Could you explain what has changed compared to the roadmap of ~2 months ago? The only change I see is the wording of the title. But it is still in the "Future" section. Just like it was 2 months ago.

@MBuchalik perhaps it's been there for 2 months.

Was this page helpful?
0 / 5 - 0 ratings