Angular introduced Model-driven forms with its FormBuilder class, whose primary method group
has a signature like this:
group(controlsConfig: {
[key: string]: any;
}): FormGroup;
The any
is actually an array with the format:
[
initial value of model's property,
sync validator(s),
async validator(s)
]
Where only the first element is required.
I decide I'd like something a little more strongly typed than that, particularly on anything which is associated with a strongly typed Model, so I re-define the function in terms of T:
declare interface FormBuilder2 extends FormBuilder {
group<T>(controlsConfig: {
[K in keyof T]?: [T[K], ValidatorFn | ValidatorFn[] | null, ValidatorFn | ValidatorFn[] | null];
}): FormGroup;
}
This also means that all my formControlNames in the HTML (and of course here in the group() call) must match the model's properties, which I prefer.
It seems to work but for one snafu:
this.optionsForm = this.formBuilder2.group<CustomerModel>({
status: [this.model.status, [Validators.required], null],
lastOrder: [this.model.lastOrder, null, null],
comments: [this.model.comments, null, null],
});
I must provide null
on the unused array slots.
Is there a way to get Typescript to omit the need for the extraneous null
s?
There's no really type-safe way to do this with tuple types, because of the way tuples can accept extra elements. That is, for example, the tuple type [A, B, C]
will actually accept additional elements of type A | B | C
(see docs).
However, there is a solution! (See attempt 3 below)
(By the way, you've overlooked that Angular has a difference interface for async validators: AsyncValidatorFn
.)
Attempt 1:
[K in keyof T]?: [T[K] | ValidatorFn | ValidatorFn[] | null];
Hardly better than any
typing (possibly worse, because it looks misleadingly meaningful).
Attempt 2:
[K in keyof T]?:
[T[K]] |
[T[K], ValidatorFn | ValidatorFn[]] |
[T[K], ValidatorFn | ValidatorFn[] | null, AsyncValidatorFn | AsyncValidatorFn[]];
Seems better at first glance. But the problem is the Typescript compiler will only throw an error as a last resort. So it will accept this:
someStringField: ['hi', 'hello']
Because this conforms to [T[K]]
(as tuples in Typescript are allowed to have extra elements).
Attempt 3:
There is a better solution, much to my amazement. I found out about this halfway through writing this answer, while reading this issue on the Typescript GitHub repo.
[K in keyof T]?: {
0: T[K];
1?: ValidatorFn | ValidatorFn[];
2?: AsyncValidatorFn | AsyncValidatorFn[];
};
This is an improvement on the previous attempt in that the first three elements are always type-checked properly. ['hi', 'hello']
gives a compile error, correctly. Additional elements are allowed and can be anything, as per usual structural typing, but that's ok.
Hope this solves your problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With