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.
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'));