@ripetchor/vivi
v0.0.13
Published
WARNING: This package is primarily intended for personal use. It may contain bugs or unexpected behavior. Use at your own risk.
Readme
Vivi
WARNING: This package is primarily intended for personal use. It may contain bugs or unexpected behavior. Use at your own risk.
Primitive Validators
v.string(options?)v.number(options?)v.boolean(options?)v.bigint(options?)v.null(options?)v.undefined(options?)
Other Validators
v.unknown()v.fallback(validator, defaultValue)v.email(options?)v.nullable(validator)v.optional(validator)v.literal(value, options?)v.date(options?)v.union([validators], options?)v.instance(Class, options?)v.object(shape, options?)v.array(validator, options?)v.map(keyValidator, valueValidator, options?)v.set(validator, options?)v.refine(validator, refinementFns[], options?)v.pipe(validator, ...transformFns)
Usage Examples
import { v } from '@ripetchor/vivi'
// or
import v from '@ripetchor/vivi'
// or
import { stringValidator, numberValidator, refine /* etc. */ } from '@ripetchor/vivi'Primitive validation
const name = v.string().parse('Alice');
console.log(name); // Alice
const age = v.number().parse(30);
console.log(age); // 30
const active = v.boolean().parse(true);
console.log(active); // trueNullable and optional
const nullableName = v.nullable(v.string()).parse(null);
console.log(nullableName); // null
const optionalAge = v.optional(v.number()).parse(undefined);
console.log(optionalAge); // undefinedArray validation
const numbers = v.array(v.number()).parse([1, 2, 3]);
console.log(numbers); // [1, 2, 3]
const users = v.array(v.object({ name: v.string(), age: v.number() })).parse([
{ name: "Lana", age: 28 },
{ name: "Mikaela", age: 25, friends: ["Nicole"] },
{ name: "Elsa", age: 28, job: "dev" },
]);
console.log(users); // [{ name: 'Lana', age: 28 }, { name: 'Mikaela', age: 25 }, { name: 'Elsa', age: 28 }]Object validation
const exactUser = v.object({ name: v.string(), age: v.number() }, { mode: 'exact' }).parse({ name: 'Alice', age: 25 });
console.log(exactUser); // { name: 'Alice', age: 25 }
const stripUser = v.object({ name: v.string() }, { mode: 'strip' }).parse({ name: 'Bob', age: 30 });
console.log(stripUser); // { name: 'Bob' }
const passthroughUser = v.object({ name: v.string() }, { mode: 'passthrough' }).parse({ name: 'Charlie', age: 40 });
console.log(passthroughUser); // { name: 'Charlie', age: 40 }Union validation
const value = v.union([v.string(), v.number()]).parse('Hello');
console.log(value); // 'Hello'Literal validation
const l1 = v.literal("yes").parse("yes");
console.log(l1); // 'yes'
v.literal("yes").parse("yeS");
// errorRefinements
const schema = refine(
v.string(),
[
(value) => value.length === 5 || "length must be 5",
(value) => /^\d+$/.test(value) || "contains only numbers",
],
{ abort: true },
);
const result = schema.safeParse("abc");
if (!result.success) {
console.log(result.error.issues); // [ { path: [], message: 'length must be 5' } ]
} else {
console.log(result.data);
}
// =========================================
const userSchema = v.object({
password: v.string(),
confirmPassword: v.string(),
});
const refinedUserSchema = v.refine(userSchema, [
(user) =>
user.password === user.confirmPassword || {
path: ["confirmPassword"],
message: "Passwords do not match",
},
]);
const result = refinedUserSchema.safeParse({
password: "secret123",
confirmPassword: "secret321",
});
console.log(result); // [ { path: ["confirmPassword"], message: "Passwordsdo not match"} ]true→ validation passesfalse→ generic "Refinement failed"string→ error message with empy path{ message, path }→ precise error targeting
Fallback
const fallbackValue = v.fallback(v.number(), 42).parse('invalid');
console.log(fallbackValue); // 42Email validation
const email = v.email().parse('[email protected]');
console.log(email); // '[email protected]'Date validation
const birthday = v.date().parse(new Date('1990-01-01'));
console.log(birthday); // 1990-01-01T00:00:00.000ZPipe transformations
const transformed = v.pipe(
v.string(),
(s: string) => s.trim(),
(s: string) => s.toUpperCase(),
).parse(' hello ');
console.log(transformed); // 'HELLO'Safe parsing
const result1 = v.number().safeParse(123);
console.log(result1); // { success: true, data: 123 }
const result2 = v.number().safeParse('not a number');
console.log(result2); // { success: false, error: ViviError }Abort on first issue
try {
v.array(v.number(), { abort: true }).parse([1, "x", 3, "y"]);
} catch (err) {
if (err instanceof ViviError) {
console.log("Aborted on first error:", err.issues);
}
}Pattern matching (match)
match is a alternative to if / else for handling the result of safeParse.
enforces handling of all possible states
provides proper type narrowing
eliminates if (result.success) checks
const schema = v.string();
const result = schema.safeParse("hello");
v.match(
result,
(data) => {
console.log("Success:", data);
},
(error) => {
console.log("Validation failed:", error.issues);
},
);
// Еhe return type is inferred from the callbacks.
const value = v.match(
result,
(data) => data.toUpperCase(),
() => "DEFAULT",
);
// value: stringType inference
const userValidator = v.object({ name: v.string(), age: v.number() });
type User = v.infer<typeof userValidator>;
const user: User = { name: 'Alice', age: 25 };
console.log(user); // { name: 'Alice', age: 25 }Issues utils
import { v, flattenErrors, issuesToObject, issuesToString } from "@ripetchor/vivi";
const schema = v.object({
user: v.object({
name: v.string(),
email: v.email(),
address: v.object({
street: v.string(),
city: v.string(),
zip: v.string(),
}),
}),
credentials: v.object({
password: v.string(),
confirmPassword: v.string(),
}),
agreeToTerms: v.literal(true),
});
const invalidData = {
user: {
name: 1,
email: 2,
address: {
street: 3,
city: 4,
zip: 5,
},
},
credentials: {
password: 6,
confirmPassword: 7,
},
agreeToTerms: false,
};
const result = schema.safeParse(invalidData);
console.log(issuesToString(result.error.issues));
// user.name: Expected string
// user.email: Expected string
// user.address.street: Expected string
// user.address.city: Expected string
// user.address.zip: Expected string
// credentials.password: Expected string
// credentials.confirmPassword: Expected string
// agreeToTerms: Expected literal true
console.log(issuesToObject(result.error.issues));
// {
// user: {
// name: 'Expected string',
// email: 'Expected string',
// address: {
// street: 'Expected string',
// city: 'Expected string',
// zip: 'Expected string'
// }
// },
// credentials: { password: 'Expected string', confirmPassword: 'Expected string' },
// agreeToTerms: 'Expected literal true'
// }
console.log(flattenErrors(result.error.issues));
// {
// 'user.name': 'Expected string',
// 'user.email': 'Expected string',
// 'user.address.street': 'Expected string',
// 'user.address.city': 'Expected string',
// 'user.address.zip': 'Expected string',
// 'credentials.password': 'Expected string',
// 'credentials.confirmPassword': 'Expected string',
// agreeToTerms: 'Expected literal true'
// }Custom validator
import { customValidator, ViviError, type ParseFn, type Path } from '@ripetchor/vivi';
interface User {
name: string;
age: number;
email: string;
}
const parseFn: ParseFn<User> = (input: unknown, path: Path): User => {
if (typeof input !== "object" || input === null) {
throw ViviError.single("Expected object", path);
}
const obj = input as Record<string, unknown>;
const name = obj.name;
if (typeof name !== "string" || name.trim() === "") {
throw ViviError.single("Name must be a non-empty string", [...path, "name"]);
}
const age = obj.age;
if (typeof age !== "number" || age < 0) {
throw ViviError.single("Age must be a non-negative number", [...path, "age"]);
}
const email = obj.email;
if (typeof email !== "string" || !/^\S+@\S+\.\S+$/.test(email)) {
throw ViviError.single("Invalid email", [...path, "email"]);
}
return { name: name.trim(), age, email };
};
const userValidator = customValidator<User>(parseFn);
const result = userValidator.safeParse({ name: " Alice ", age: 25, email: "[email protected]" });
if (result.success) {
console.log(result.data); // { name: "Alice", age: 25, email: "[email protected]" }
}