DEV Community

Jrubzjeknf
Jrubzjeknf

Posted on • Updated on

Using a single interface with Angular Typed Forms

In my previous post about typed forms I mentioned two utility types which make it easier to work with typed forms.

Here I'll make a three recommendations that will make it a LOT easier to work with typed forms.

🔧 Create a type for your FormGroups

Like this:

type UserForm = FormGroup<{
    name: FormControl<string | null>;
}>;
Enter fullscreen mode Exit fullscreen mode

A typed form like this is much easier to use in code and pass around between components when you have to.


@Component({
    selector: 'my-form',
    template: ` {{ userForm.value }} `,
})
export class MyFormComponent {
    @Input() userForm!: UserForm;
}

@Component({
    selector: 'my-app',
    template: ` <my-form [userForm]="userForm"></my-form>`,
})
export class MyAppComponent {
    userForm: UserForm = new FormGroup(...);
}
Enter fullscreen mode Exit fullscreen mode

You can nest your forms more easily too.

type AddressForm = FormGroup<{
    street: FormControl<string | null>
}>

type UserForm = FormGroup<{
    name: FormControl<string | null>;
    address?: AddressForm
}>;
Enter fullscreen mode Exit fullscreen mode

This way you get concise and clean code, which makes anyone a happy developer. 👍
It also makes it much easier to instantiate FormGroups, since you can infer the types of the controls and the value. We just need a bit of help.

🔨 Infer the control types of your FormGroup

Creating a FormGroup without providing a type causes issues. For example, this throws an error:

type UserForm = FormGroup<{
    name: FormControl<string | null>;
}>;

const userForm: UserForm = new FormGroup({
    name: new FormControl(null)
})
Enter fullscreen mode Exit fullscreen mode

/sadface

It doesn't know name is of type FormControl<string | null>, because Typescript can't infer that. We need to tell what kind of controls our FormGroup exists of, and we need to use a utility type.

/**
 * Produces the controls for a typed FormGroup or FormArray.
 * Can be used to create a new FormGroup or FormArray.
 *
 * @example const myForm: MyForm = new FormGroup<Controls<MyForm>>({...});
 */
export type Controls<TAbstractControl> = TAbstractControl extends FormGroup<infer TControls>
    ? {
            [K in keyof TControls]: TControls[K];
      }
    : TAbstractControl extends FormArray<infer TControls>
    ? TControls[]
    : TAbstractControl extends FormControl
    ? TAbstractControl
    : never;

type UserForm = FormGroup<{
    name: FormControl<string | null>;
    address?: AddressForm
}>;

const userForm: UserForm = new FormGroup<Controls<UserForm>>({
    name: new FormControl(null)
})
Enter fullscreen mode Exit fullscreen mode

This works wonderfully! name is now a FormControl<string | null> and the code compiles. The additional benefit is that, when making a mistake in the FormControl's type, an error is shown on the control and not the entire group. This makes hunting down errors much quicker and easier.

🛠 Enable the strictTemplates compiler option

Because we can infer the control types and value type from our own FormGroup type, we create a wonderful developer experience where everything is strongly typed and with brevity! With the strictTemplates (or the deprecated fullTemplateTypeCheck) compiler option on, your components are strongly typed as well. As a bonus, you can quickly navigate to controls and values using F12 (Go To Definition), because it relates those types!

To take full advantage, do this:

  • Prefer navigating to controls using userForm.controls.address instead of userForm.get('address'). The latter will not warn you in case of mistakes. Deep selecting can become tedious (userForm.controls.address.controls.street instead of userForm.get('address.street'), but it is type safe, so make your own decision what you find more important;

  • For FormGroups you use in multiple files, create a type and create your FormGroup with new FormGroup<Controls<...>>(...) or with a FormBuilder: fb.group<Controls<...>>(...);

  • If you use a FormBuilder, you have to use fb.control(...) for the controls. The shorthand for creating controls unfortunately doesn't work well with the typed controls.

  • As mentioned in the previous article, be mindful of the FormValue's type: all its properties are optional, because controls can be disabled, and you must choose how to deal with that.

💻 Code example

I've created a StackBlitz with a single file containing the types and a code example. Don't look at the suggestions of StackBlitz, it pales in comparison to VS Code. You can paste the file straight into any Angular project's .ts file and it will work with correct typing. Be sure to have strictTemplates enabled in order to get type information in the component's template.

Thanks for reading!

I hope it can help you making your code base a tad more type safe. 😊

Top comments (0)