javascript / expert
Snippet
Exhaustive Reactive Form Validators with Cross-Field Error Typing
Angular's `ValidationErrors` type is `{ [key: string]: any }`, which means a validator can attach any shape under any key and the type system will not stop a consumer from misreading it — the safety has to be built by convention, not inferred. Declaring `DateRangeErrors` as a named interface and constructing the exact literal shape it describes before returning it turns the validator's contract into something a code reviewer or future refactor can check by eye, and `readRangeError`'s return type documents precisely what shape callers should expect back from `form.errors?.['rangeInverted']`, even though `errors` itself stays untyped at the framework boundary.
snippet.js
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { FormGroup, FormControl, ValidatorFn, ValidationErrors } from '@angular/forms';interface DateRangeErrors {rangeInverted: { start: string; end: string };}function dateRangeValidator(): ValidatorFn {return (group): ValidationErrors | null => {const start = group.get('start')?.value as string | null;const end = group.get('end')?.value as string | null;if (!start || !end) {return null;}if (new Date(start) > new Date(end)) {const error: DateRangeErrors = {rangeInverted: { start, end },};return error;}return null;};}const form = new FormGroup({start: new FormControl<string | null>(null),end: new FormControl<string | null>(null),},{ validators: dateRangeValidator() },);function readRangeError(): DateRangeErrors['rangeInverted'] | undefined {return form.errors?.['rangeInverted'];}
angular
Breakdown
1
interface DateRangeErrors { rangeInverted: { start: string; end: string }; }
Names the exact error shape as documentation and a refactor anchor, even though Angular's `ValidationErrors` type cannot enforce it structurally.
2
const start = group.get('start')?.value as string | null;
`AbstractControl.value` is typed generically as the control's value type, so cross-field access through `.get()` still needs an explicit assertion here.
3
const error: DateRangeErrors = { rangeInverted: { start, end } };
Assigning to the named interface before returning catches a typo in the error key or shape at compile time instead of at the template that reads it.
4
function readRangeError(): DateRangeErrors['rangeInverted'] | undefined
Uses an indexed access type to extract just the nested shape, giving callers a precise contract without re-declaring the inner interface.