question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

(forms): Problems with typed forms and generics/inheritance.

See original GitHub issue

Which @angular/* package(s) are the source of the bug?

forms

Is this a regression?

No

Description

We have some complex forms throughout the apps where we use inheritance. We have created a custom typed forms at some point (quite long time ago) and now would like to switch to typed forms.

However, the types seem to be incorrectly defined as quite trivial code can’t be accepted by FormGroup, for example. Here’s the sample code. Cannot create a stackblitz reproduction because for some reason I wasn’t able to find a typed FormGroup and FormControl even though the angular package versions in stackblitz were 14.1.1.

export interface IAnimalBase {
    isFish: FormControl<boolean>;
    isNotFish: FormControl<boolean>;
}

export interface IAnimalForm<TAnimalBasics extends IAnimalBase = IAnimalBase> {
    animalBasics: FormGroup<TAnimalBasics>;
}

export interface IDogForm extends IAnimalForm {
    dogStuff: FormGroup<{ canBark: FormControl<boolean> }>;
}

class Test<TFG extends IAnimalForm> {
    form: FormGroup<TFG>;
}

Please provide the exception or error you saw

image

image

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 14.1.0
Node: 14.17.5
Package Manager: yarn 1.22.18
OS: win32 x64

Angular: 14.1.0
... animations, cdk, cli, common, compiler, compiler-cli, core
... forms, language-service, material, platform-browser
... platform-browser-dynamic, router

Package                            Version
------------------------------------------------------------
@angular-devkit/architect          0.1401.0
@angular-devkit/build-angular      14.1.0
@angular-devkit/build-ng-packagr   0.1002.0
@angular-devkit/core               14.1.0
@angular-devkit/schematics         14.1.0
@angular/flex-layout               14.0.0-beta.40
@schematics/angular                14.1.0
rxjs                               7.5.6
typescript                         4.6.3

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:9 (3 by maintainers)

github_iconTop GitHub Comments

4reactions
alxhubcommented, Aug 10, 2022

Hi @liesahead,

The error TypeScript is reporting here is actually correct and valid - but it’s definitely not intuitive why this is the case.

To understand it, let’s first look at the constraint on FormGroup’s type:

export class FormGroup<TControl extends {[K in keyof TControl]: AbstractControl<any>} = any> {...}

This says that the control group TControl must be an object, and for any property of that object (any key in keyof TControl), the value of that property must be an AbstractControl instance.

This makes sense - TControl is the control type for the FormGroup, so its values should all be AbstractControls.

We can see why this works:

export interface IAnimalBase {
    isFish: FormControl<boolean>;
    isNotFish: FormControl<boolean>;
}

const fg = new FormGroup<IAnimalBase>(...); // legal!

We’re passing the interface IAnimalBase for TControl, which has two keys (isFish and isNotFish), both of which have proper AbstractControl-compatible values. So this is accepted by FormGroup’s generic constraint.

Now, onto the example that doesn’t work:

export interface IAnimalForm<TAnimalBasics extends IAnimalBase = IAnimalBase> {
    animalBasics: FormGroup<TAnimalBasics>; // error! does not satisfy FormGroup's constraint
}

This already causes an error - FormGroup is not happy with the type of TAnimalBasic. To understand why FormGroup is right not to accept this type, let’s look at an example of what could break if it did:

interface BrokenAnimalControls extends IAnimalBase {
  isBroken: string; // NOT an AbstractControl type
}

const fg = new FormGroup<IAnimalForm<BrokenAnimalControls>>(...);
fg.controls.animalBasics.controls.isBroken // <-- NOT an AbstractControl type!

What happened here:

  • BrokenAnimalControls extends IAnimalBase and adds an isBroken property alongside isFish and isNotFish.
  • isBroken is NOT any kind of `AbstractControl - the forms system won’t know what to do with it
  • IAnimalForm didn’t mind us adding isBroken - it only constrains us to extend IAnimalBase, which we do.
  • FormGroup has no way of understanding or handling the isBroken property, since it’s not a control.

The problem is that even though the FormGroup constraint is valid for IAnimalBase itself, that doesn’t guarantee it’ll be valid for any type derived from IAnimalBase. This is why FormGroup rejects such arbitrary derived types.

To make this work, we need to enforce the same constraint as FormGroup about AbstractControl values when declaring our derived type constraints:

export interface IAnimalForm<TAnimalBasics extends IAnimalBase & {[key in keyof TAnimalBasics]: AbstractControl<any>} = IAnimalBase> {
    animalBasics: FormGroup<TAnimalBasics>; // no more error
}

With that change, TAnimalBasics now provides the guarantee that FormGroup requires - all of its defined properties must have AbstractControl values, no matter what actual type is passed in to satisfy it. FormGroup accepts it as a result.

It might be easier to define a helper for this:

export type DerivedControlType<TControl> = {[key in keyof TControl]: AbstractControl<any>};
export interface IAnimalForm<TAnimalBasics extends IAnimalBase & DerivedControlType<TAnimalBasics> = IAnimalBase> {
    animalBasics: FormGroup<TAnimalBasics>; // no more error
}

I hope this explanation makes sense - please let us know. There are definitely subtle things about the typed forms interfaces that are non-intuitive like this, and it’d be a good idea for us to have some documentation around the ones users like yourself end up encountering.

0reactions
liesaheadcommented, Dec 15, 2022

@alxhub , Maybe there’s a chance to somehow improve the generic forms implementation by using new typescript satisfies operator?

Read more comments on GitHub >

github_iconTop Results From Across the Web

Generic inheritance on Forms - MSDN - Microsoft
I have this situation: -Cne "Car" class. ... The result is an error on the Designer saying: "Cannot create an instance of BaseForm[T]...
Read more >
c# - Visual Studio 2019 / Generic Forms + Inheritance Issues
1.) GenericArguments[0], 'Project.DBConnection.PurchaseOrder', on Project.Client.Forms.EntityPage'1[T]' violates the constraint of type ...
Read more >
Issues with Generic Windows Forms inheritance ... - EMoreau
The .Net Framework itself has no issues at all with you inheriting a generic form because for the framework, a form is just...
Read more >
Restrictions on Generics (The Java™ Tutorials > Learning the ...
Cannot Instantiate Generic Types with Primitive Types; Cannot Create Instances of Type ... For example, the following code causes a compile-time error:.
Read more >
4 Problems of Angular 14 Typed Forms and How to Fix Them
You can't get FormGroup or FormArray from your models' types. But in real-world forms, bind with models. You should be able to construct...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found