Using a single interface with Angular Typed Forms
Tags: angular, typescript
Published: 2022-07-29 13:27:00

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:

```ts
type UserForm = FormGroup<{
	name: FormControl<string | null>;
}>;
```
A typed form like this is much easier to use in code and pass around between components when you have to.

```ts

@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(...);
}
```

You can nest your forms more easily too.

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

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

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:

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

const userForm: UserForm = new FormGroup({
	name: new FormControl(null)
})
```

![/sadface](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mwyvftpc1e8zju22neyc.png)
 
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.

```ts
/**
 * 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)
})
```

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](https://dev.to/jrubzjeknf/making-typed-forms-a-bit-more-consistent-353l), 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](https://stackblitz.com/edit/angular-ivy-cosiht?file=src%2Fapp%2Ftyped-forms-supercharged.ts&view=editor) 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. 😊