Narrowing a union of strictly typed forms in Angular

Discussão em 'Angular' iniciado por Alex Davis, Janeiro 14, 2025.

  1. Alex Davis

    Alex Davis Guest

    I have a form which has three separate instances, but at its core has the same data with one or two different properties. I wanted to have a base form class which takes a generic argument to narrow the types and remove some controls where they are not needed, but I have been fighting Typescript for hours and am close to giving up and just using untyped forms.

    I have created a smaller example to show the problem:

    export type FormOne = {
    name: AbstractControl<string | null>;
    email: AbstractControl<string | null>;
    age: AbstractControl<number | null>;
    };

    export type FormTwo = {
    name: AbstractControl<string | null>;
    email: AbstractControl<string | null>;
    };

    export type FormThree = {
    name: AbstractControl<string | null>;
    email: AbstractControl<string | null>;
    location: AbstractControl<string | null>;
    };

    export type BaseForm = FormOne | FormTwo | FormThree;



    I wanted to define my base class like so:

    @Directive()
    export abstract class BaseFormComponent<T extends BaseForm> {
    private readonly _formBuilder = inject(FormBuilder);

    public readonly baseForm = this._formBuilder.group<T>({
    name: this._formBuilder.control<string | null>(''),
    email: this._formBuilder.control<string | null>(''),
    age: this._formBuilder.control<number | null>(0),
    location: this._formBuilder.control<string | null>(''),
    });
    }


    but come across the TS2345 error:

    TS2345: Argument of type

    {
    name: FormControl<string | null>;
    email: FormControl<string | null>;
    age: FormControl<number | null>;
    location: FormControl<string | null>;
    }

    is not assignable to parameter of type T

    {
    name: FormControl<string | null>;
    email: FormControl<string | null>;
    age: FormControl<number | null>;
    location: FormControl<string | null>;
    }

    is assignable to the constraint of type T, but T could
    be instantiated with a different subtype of constraint BaseForm.


    This confuses me because there are no other subtypes of BaseForm than the three I have defined, and my understanding of union types is that BaseForm would resolve to:

    {
    name: AbstractControl<string | null>;
    email: AbstractControl<string | null>;
    age?: AbstractControl<number | null>;
    location?: AbstractControl<string | null>;
    }


    which should allow the base form as I defined it in the abstract class. The end goal would be to have components like:

    @Component({...})
    export class FormTwoComponent extends BaseFormComponent<FormTwo> implements OnInit {
    public ngOnInit(): void {
    this.baseForm.removeControl('age');
    this.baseForm.removeControl('location');
    }
    }


    Now I realise that the component above has a form which doesn't conform to the given T until ngOnInit runs, and so this might cause compile-time problems of its own, but I haven't even got so far as defining the base class.

    Can anyone more versed in Typescript explain to me how I am going wrong, and how this could be done, if at all?

    StackBlitz project containing all code shown.

    Continue reading...

Compartilhe esta Página