Creates a resuable rules that can validate multiple inputs, returning a valid and cleaned-up value or an accumlation of errors.
Last active
November 26, 2025 19:04
-
-
Save jmakeig/fd94814909aba4dee0a56ab988caa60e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| type Maybe<T> = T | unknown; | |
| type Invalid<V = unknown> = { input: V; issues: string[]; } | |
| type MaybeInvalid<T> = Maybe<T> | Invalid; | |
| function is_invalid<V>(result: unknown): result is Invalid<V> { | |
| return 'object' === typeof result | |
| && null !== result | |
| && 'input' in result | |
| && 'issues' in result | |
| && Array.isArray(result.issues); | |
| } | |
| class Schema<T> { | |
| #steps: Array<(v: unknown) => MaybeInvalid<T>> = []; | |
| constructor() { } | |
| exists(message?: string): this { | |
| this.#steps.push((value) => Schema.exists(value, message)); | |
| return this; | |
| } | |
| string(trim = false, message?: string): this { | |
| this.#steps.push((value) => Schema.string(value, trim, message)); | |
| return this; | |
| } | |
| length(min: number, max?: number, message?: string): this { | |
| this.#steps.push((value) => Schema.len(value, min, message)); | |
| return this; | |
| } | |
| validate<T>(value: unknown): MaybeInvalid<T> { | |
| const issues = []; | |
| let result = value, i = 0; | |
| for (const step of this.#steps) { | |
| result = step(result); | |
| if (is_invalid(result)) { | |
| issues.push(...result.issues); | |
| result = result.input; | |
| } | |
| console.log(`Step ${i++}`, result); | |
| } | |
| if (0 === issues.length) return result; | |
| return { | |
| input: value, | |
| issues | |
| } | |
| } | |
| // Static | |
| static exists<T>(value: unknown, message: string = 'Doesn’t exist'): MaybeInvalid<T> { | |
| if (undefined === value || null === value) { | |
| return { | |
| input: value, | |
| issues: [message] | |
| } | |
| } | |
| return value; | |
| } | |
| static string(value: unknown, trim = false, message: string = `Needs to be a string`): MaybeInvalid<string> { | |
| // Precondition | |
| if (!Schema.exists(value)) { | |
| return { | |
| input: value, | |
| issues: [] | |
| } | |
| } | |
| if ('string' === typeof value) { | |
| return trim ? value.trim() : value; | |
| } | |
| return { | |
| input: value, | |
| issues: [message] | |
| } | |
| } | |
| static len(value: unknown, min: number = 0, message: string = `Not at least ${min} long`): MaybeInvalid<string> { | |
| // Precondition | |
| if (is_invalid(Schema.exists(value)) || is_invalid(Schema.string(value))) { | |
| // console.error('len precondition failed'); | |
| return { | |
| input: value, | |
| issues: [] | |
| } | |
| } | |
| if ('string' === typeof value && value.length >= min) { | |
| return value; | |
| } | |
| return { | |
| input: value, | |
| issues: [message] | |
| }; | |
| } | |
| } | |
| const name_schema = new Schema() | |
| .exists() | |
| .string(true) | |
| .length(10); | |
| // console.log(name_schema.validate('This is a lot of text that should be valid.')); | |
| console.log(name_schema.validate(44)); | |
| // console.log(name_schema.validate('4 ')); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment