Search code examples
angularangular-forms

Angular Typed Forms Can't Assign To Partial Type


Made a basic example to show what I'm trying to accomplish:

Basically given these two types:

type BaseType = FormGroup<{
  user: FormControl<string>;
}>;

type SomeOtherType = FormGroup<{
  user: FormControl<string>;
  amount: FormControl<number>;
}>;

I want to be able to pass SomeOtherType to a Component's input of type BaseType since they fulfill the contract.

Attempting this normally I get an seemingly backwards error:

Type 'SomeOtherType' is not assignable to type 'BaseType'.
Property 'amount' is missing in type '{ user: FormControl<string>; }' but required in type '{ user: FormControl<string>; amount: FormControl<number>; }'.

I can get wipe out all restrictions by using an input transform

@Input({ required: true, transform: (value: FormGroup): BaseType => value })
form!: BaseType;

But I don't like how then any form group can be passed when I want it restricted to objects that match the base type.

(To help head off any questions as to why not just do it some other way. This is part of an already finished product and I'm just trying to migrate it from the untyped forms everywhere to the new typed forms. With untyped forms, everything functions perfectly and the components that rely on a subset of the form controls have no issues, just hoping to be able to keep that functionality but while using the new typing features.)


Solution

  • There's something about the definition of FormGroup that doesn't allow covariance with a subtype of form controls. In other words while {user: FormControl, amount: FormControl} is a subtype of {user: FormControl}, FormGroup<{user: FormControl, amount: FormControl}> IS NOT a subtype of FormGroup<{user: FormControl>.

    FormGroup extends AbstractControl and it appears your problem is due to how the the value of the control is derived. (This conclusion was reached by trial and error as the type gymnastics going on is inscrutable.)

    Since the issue comes down to how FormGroup extends AbstractControl, we can use a discriminated union to make BaseType a bit more flexible while still maintaining pretty good type definitions.

    type BaseTypeValue = {
      user: string;
    };
    
    type BaseTypeFormControls = { 
      [K in keyof BaseTypeValue]: FormControl<BaseTypeValue[K]> 
    }
    
    type BaseType = 
      FormGroup<BaseTypeFormControls>
      | AbstractControl<Partial<BaseTypeValue>, BaseTypeValue>;
    

    StackBlitz