Using a single interface with Angular Typed Forms
Jrubzjeknf
Posted on July 29, 2022
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>;
}>;
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(...);
}
You can nest your forms more easily too.
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:
type UserForm = FormGroup<{
name: FormControl<string | null>;
}>;
const userForm: UserForm = new FormGroup({
name: new FormControl(null)
})
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)
})
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 ofuserForm.get('address')
. The latter will not warn you in case of mistakes. Deep selecting can become tedious (userForm.controls.address.controls.street
instead ofuserForm.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. 😊
Posted on July 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 20, 2024
November 15, 2024