javascript / expert
Snippet
Composable Validator Factories Using Higher-Order Functions for Reusable Constraints
Rather than hand-writing a monolithic validator per form, this pattern treats validators as first-class values produced by parameterized factory functions, then combines them with a composeAnd higher-order function that merges every non-null ValidationErrors object into one. Each factory closes over its configuration (a count, a sibling control path) and returns a pure ValidatorFn, so the same minDistinctChars(4) can be reused across unrelated forms with different thresholds, and composeAnd works with any number of validators without the caller needing to know how many failed simultaneously.
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
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';function minDistinctChars(count: number): ValidatorFn {return (control: AbstractControl): ValidationErrors | null => {const value: string = control.value ?? '';const distinct = new Set(value.split('')).size;return distinct >= count ? null : { minDistinctChars: { required: count, actual: distinct } };};}function composeAnd(...validators: ValidatorFn[]): ValidatorFn {return (control: AbstractControl): ValidationErrors | null => {return validators.reduce<ValidationErrors | null>((acc, validate) => {const result = validate(control);return result ? { ...acc, ...result } : acc;}, null);};}function notEqualTo(otherControlPath: string): ValidatorFn {return (control: AbstractControl): ValidationErrors | null => {const sibling = control.parent?.get(otherControlPath);if (!sibling) return null;return control.value === sibling.value ? { notEqualTo: { path: otherControlPath } } : null;};}const passwordValidator = composeAnd(minDistinctChars(4), notEqualTo('username'));
angular
Breakdown
1
function minDistinctChars(count: number): ValidatorFn {
The factory returns a function rather than being a validator itself, capturing count in a closure available at every future invocation.
2
const distinct = new Set(value.split('')).size;
A Set deduplicates characters in one pass, giving an O(n) distinct-count without manual bookkeeping.
3
function composeAnd(...validators: ValidatorFn[]): ValidatorFn {
Rest parameters let composeAnd accept an arbitrary number of validators, making it a generic combinator rather than fixed-arity glue code.
4
return result ? { ...acc, ...result } : acc;
Spreading merges error keys from multiple failing validators instead of only surfacing the last one, so the UI can show every violated rule.
5
const sibling = control.parent?.get(otherControlPath);
Optional chaining guards against calling this validator before the control is attached to its parent FormGroup, when parent is still null.