Reactive forms in angular are a very powerful way to create forms. Unfortunately, the form builder does not support any type safety.
Small typos can go unnoticed and using models for multiple forms can be frustrating to maintain.
But thanks to TypeScript there are some ways to overcome those issues :).
The problem
interface Person {
firstname: string;
lastname: string;
}
export class AppComponent {
form: FormGroup;
constructor(fb: FormBuilder) {
this.form = fb.group({
firstName: [null],
lastNamee: [null]
});
}
}
By creating a form the default way, some general issues are not covered. The form does not match the defined model. At the latest when using your form value with an API or using the formControlName
directive you might notice the error.
Using Record
Record<K, P>
is a utility type provided by TypeScript. It requires two type arguments (documentation).
interface Person {
firstname: string;
lastname: string;
}
export class AppComponent {
form: FormGroup;
constructor(fb: FormBuilder) {
const form: Record<keyof Person, any> = {
firstname: [null],
lastname: [null]
};
this.form = fb.group(form);
}
}
Using a custom type
If you'd like to avoid repeating the Record<keyof T, any>
syntax, you can declare a type
for general uses. This custom type can also be declared more specific. But for now, we go with the any
type to accomplish the same result as before.
type FormGroupModel<T> = {
[x in keyof T]: any;
}
interface Person {
firstname: string;
lastname: string;
}
export class AppComponent {
form: FormGroup;
constructor(fb: FormBuilder) {
const form: FormGroupModel<Person> = {
firstname: [null],
lastname: [null]
};
this.form = fb.group(form);
}
}
Dive deeper
To make it more type safe, like passing the correct value or validators, we can enhance the FormGroupModel
a little bit more.
type ValidatorModel<T, K> = T | K | (T | K)[];
type FormGroupModel<T> = {
[x in keyof T]: [
T[x],
ValidatorModel<Validator, ValidatorFn>?,
ValidatorModel<AsyncValidator, AsyncValidatorFn>?
];
};
Changing the any
type to this ominous code makes it more restricted. The type now accepts an array with up to 3 arguments.
The first argument in the array (T[x]
) states, that the initial value has to match with the defined model.
interface Person {
firstname: string;
lastname: string;
}
const form: FormGroupModel<Person> = {
firstname: ['Randy'], // valid
lastname: [123] // invalid
};
The second argument can be a Validator or a ValidatorFn or a set of Validator and ValidatorFn.
const form: FormGroupModel<Person> = {
firstname: ['Randy', Validators.required],
lastname: ['McRandom', [Validators.required, Validators.minLength(5)]]
};
The third argument works like the second, but for AsyncValidator and AsyncValidatorFn.
Summary
With those approaches, the form is enforced to be built correctly. The biggest benefit is, that when the model changes, the compiler will complain that the form does not match. This will make your project way more maintainable 👍.
Top comments (1)
I approve 100%