0 Tage Serie
javascript / expert
Snippet

Erschöpfende reaktive Formular-Validatoren mit feldübergreifender Fehlertypisierung

Angulars Typ `ValidationErrors` ist `{ [key: string]: any }`, was bedeutet, dass ein Validator jede beliebige Form unter jedem beliebigen Schlüssel anhängen kann und das Typsystem einen Konsumenten nicht davon abhält, sie falsch zu lesen — die Sicherheit muss durch Konvention entstehen, nicht durch Inferenz. `DateRangeErrors` als benanntes Interface zu deklarieren und vor der Rückgabe genau die beschriebene literale Form zu konstruieren, macht den Vertrag des Validators zu etwas, das ein Code-Reviewer oder ein zukünftiges Refactoring mit bloßem Auge prüfen kann, und der Rückgabetyp von `readRangeError` dokumentiert präzise, welche Form Aufrufer von `form.errors?.['rangeInverted']` erwarten sollten — auch wenn `errors` selbst an der Framework-Grenze untypisiert bleibt.

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
Erklärung
1
interface DateRangeErrors { rangeInverted: { start: string; end: string }; }
Benennt die exakte Fehlerform als Dokumentation und Refactor-Anker, auch wenn Angulars `ValidationErrors`-Typ sie strukturell nicht erzwingen kann.
2
const start = group.get('start')?.value as string | null;
`AbstractControl.value` ist generisch als der Werttyp des Controls typisiert, daher braucht der feldübergreifende Zugriff über `.get()` hier weiterhin eine explizite Assertion.
3
const error: DateRangeErrors = { rangeInverted: { start, end } };
Die Zuweisung an das benannte Interface vor der Rückgabe fängt einen Tippfehler im Fehlerschlüssel oder in der Form zur Kompilierzeit ab statt erst im Template, das sie liest.
4
function readRangeError(): DateRangeErrors['rangeInverted'] | undefined
Nutzt einen Indexed-Access-Type, um nur die verschachtelte Form zu extrahieren, und gibt Aufrufern einen präzisen Vertrag, ohne das innere Interface erneut zu deklarieren.