- Strongly typed
unknown→Entity - Accumulation of all validation issues, not just the first (or the last)
- Return the prepared entity, not just
Booleanvalidation result. For example, parsing a string input as a number.
Last active
November 15, 2025 19:40
-
-
Save jmakeig/b700f21d95b012d9fb1b11e4b5be3bd8 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 Issue = Readonly<{ | |
| message: string; | |
| path?: Path; | |
| }>; | |
| type Path = ReadonlyArray<PropertyKey>; | |
| type Invalid<In> = Readonly<{ | |
| input: In; | |
| validations: ReadonlyArray<Issue>; | |
| }>; | |
| type MaybeInvalid<In, Out> = Out | Invalid<In>; | |
| type Person = { | |
| name: string; | |
| age: number; | |
| friends: Person[]; | |
| }; | |
| /** | |
| * Checks whether a `MaybeInvalid` result is actually `Invalid`. | |
| */ | |
| function is_invalid<In, Out>(result: MaybeInvalid<In, Out>): result is Invalid<In> { | |
| return 'object' === typeof result | |
| && null !== result | |
| && 'validations' in result | |
| && Array.isArray(result.validations); | |
| } | |
| /** | |
| * * Validates the inpnut `value` as a `Person` entity | |
| * * Prepares a new copy for any translation, e.g. parsing a `string` for a `number` property | |
| * * Accumulates all of the validation issues | |
| * * Returns either the validated and prepared entity or the input along with the validation issues | |
| */ | |
| function validate_person(value: unknown, base_path: Path = ['person']): MaybeInvalid<unknown, Person> { | |
| const validations: Array<Issue> = []; | |
| function issue_add(message: Issue['message'] | ReadonlyArray<Issue>, path: PropertyKey | undefined): void { | |
| if (Array.isArray(message)) { | |
| validations.push(...message); | |
| } else if ('string' === typeof message) { | |
| validations.push({ message, path: path ? [...base_path, path] : [...base_path] }); | |
| } else { | |
| throw new TypeError(typeof message); | |
| } | |
| } | |
| const person = structuredClone(value); | |
| if ('object' === typeof person && null !== person) { | |
| if ('name' in person && 'string' === typeof person.name) { | |
| const name = person.name.trim(); | |
| if (person.name.length < 3) { | |
| issue_add('name length', 'name'); | |
| } | |
| person.name = name; | |
| } else { | |
| issue_add('name existence', 'name'); | |
| } | |
| if ('age' in person) { | |
| if ('string' === typeof person.age) { | |
| person.age = parseInt(person.age, 10); | |
| } | |
| if ('number' === typeof person.age) { | |
| const age = Math.trunc(person.age); | |
| if (age <= 0 || age > 120) { | |
| issue_add('age invalid', 'age'); | |
| } | |
| person.age = age; | |
| } else { | |
| issue_add('age invalid type', 'age'); | |
| } | |
| } else { | |
| issue_add('age existence', 'age'); | |
| } | |
| if ('friends' in person && Array.isArray(person.friends)) { | |
| for (const friend of person.friends) { | |
| const result = validate_person(friend, [...base_path, 'friends']); | |
| if (is_invalid(result)) validations.push(...result.validations); | |
| } | |
| } else { | |
| validations.push({ message: 'friends existence' }); | |
| } | |
| } else { | |
| validations.push({ message: 'existence', path: [...base_path] }); | |
| } | |
| if (validations.length > 0) return { input: value, validations }; | |
| return person as Person; | |
| } | |
| console.log(is_invalid(validate_person(null))); | |
| console.log(is_invalid(validate_person({}))); | |
| console.log(is_invalid(validate_person({ name: 'Sierra' }))); | |
| console.log(is_invalid(validate_person({ name: 'Sierra', age: -42 }))); | |
| console.log(is_invalid(validate_person({ name: 'Sierra', age: 142 }))); | |
| console.log(is_invalid(validate_person({ name: 'Sierra', age: ['42'], friends: [] }))); | |
| console.log(!is_invalid(validate_person({ name: 'Sierra', age: '42', friends: [] }))); | |
| console.log(!is_invalid(validate_person(validate_person({ name: 'Sierra', age: '42', friends: [] })))); // validates the returned entity | |
| console.log(is_invalid(validate_person({ name: 'Sierra', age: '42', friends: [{ name: 'Sierra', age: '420', friends: [] }] }))); | |
| console.log(!is_invalid(validate_person({ name: 'Sierra', age: '42', friends: [{ name: 'Sierra', age: '42', friends: [] }] }))); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment