Skip to content

Instantly share code, notes, and snippets.

@jmakeig
Last active November 15, 2025 19:40
Show Gist options
  • Select an option

  • Save jmakeig/b700f21d95b012d9fb1b11e4b5be3bd8 to your computer and use it in GitHub Desktop.

Select an option

Save jmakeig/b700f21d95b012d9fb1b11e4b5be3bd8 to your computer and use it in GitHub Desktop.

Validation

  • Strongly typed unknownEntity
  • Accumulation of all validation issues, not just the first (or the last)
  • Return the prepared entity, not just Boolean validation result. For example, parsing a string input as a number.
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