Skip to content

Instantly share code, notes, and snippets.

@freddi301
Created May 22, 2024 00:16
Show Gist options
  • Save freddi301/6dfa48b7473926f933d31c8e9d766236 to your computer and use it in GitHub Desktop.
Save freddi301/6dfa48b7473926f933d31c8e9d766236 to your computer and use it in GitHub Desktop.
typescript validation library experiment
export type Outcome<Value, Report> = { type: "valid"; value: Value } | { type: "invalid"; reports: Array<Report> };
export type ValidatorSyncBase<Value, Report> = {
validateSync(value: unknown): Outcome<Value, Report>;
};
export function makeValidatorSync<const Value, const Report>(validatorSyncBase: ValidatorSyncBase<Value, Report>) {
return {
...validatorSyncBase,
refine: refine(validatorSyncBase),
};
}
export type Output<V extends ValidatorSyncBase<any, any>> = V extends ValidatorSyncBase<infer O, any> ? O : never;
export type Report<V extends ValidatorSyncBase<any, any>> = V extends ValidatorSyncBase<any, infer R> ? R : never;
export function unknown_() {
return makeValidatorSync<unknown, undefined>({
validateSync(value): Outcome<unknown, undefined> {
return { type: "valid", value };
},
});
}
export function undefined_<const R>(reports: Array<R>) {
return makeValidatorSync<undefined, R>({
validateSync(value) {
if (value === undefined) return { type: "valid", value };
else return { type: "invalid", reports };
},
});
}
export function null_<const R>(reports: Array<R>) {
return makeValidatorSync<null, R>({
validateSync(value) {
if (value === null) return { type: "valid", value };
else return { type: "invalid", reports };
},
});
}
export function boolean<const R>(reports: Array<R>) {
return makeValidatorSync<boolean, R>({
validateSync(value) {
if (typeof value === "boolean") return { type: "valid", value };
else return { type: "invalid", reports };
},
});
}
export function number<const R>(reports: Array<R>) {
return makeValidatorSync<number, R>({
validateSync(value) {
if (typeof value === "number") return { type: "valid", value };
else return { type: "invalid", reports };
},
});
}
export function string<const R>(reports: Array<R>) {
return makeValidatorSync<string, R>({
validateSync(value) {
if (typeof value === "string") return { type: "valid", value };
else return { type: "invalid", reports };
},
});
}
export function literal<const T extends string | number | boolean, const R>(expectedLiteral: T, reports: Array<R>) {
return makeValidatorSync<T, R>({
validateSync(value) {
if (value === expectedLiteral) return { type: "valid", value: expectedLiteral };
else return { type: "invalid", reports };
},
});
}
export function array<V extends ValidatorSyncBase<any, any>, const R>(itemValidator: V, arrayReports: Array<R>) {
return makeValidatorSync<Array<Output<V>>, R | Report<V>>({
validateSync(value) {
if (!Array.isArray(value)) return { type: "invalid", reports: arrayReports };
const outcomes = value.map((item) => itemValidator.validateSync(item));
if (outcomes.some((outcome) => outcome.type === "invalid")) {
return {
type: "invalid",
reports: outcomes.flatMap((outcome) => {
if (outcome.type === "invalid") return outcome.reports;
else return [];
}),
};
} else {
return { type: "valid", value: value as Array<Output<V>> };
}
},
});
}
// TODO fail on extra keys
export function object<V extends Record<string, ValidatorSyncBase<any, any>>, const R>(
fieldValidators: V,
objectReports: Array<R>,
) {
return makeValidatorSync<{ [K in keyof V]: Output<V[K]> }, R | { [K in keyof V]: Report<V[K]> }[keyof V]>({
validateSync(value) {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return { type: "invalid", reports: objectReports };
}
const outcomes = Object.entries(fieldValidators).map(([key, validator]) =>
validator.validateSync((value as any)[key]),
);
if (outcomes.some((outcome) => outcome.type === "invalid")) {
return {
type: "invalid",
reports: outcomes.flatMap((outcome) => {
if (outcome.type === "invalid") return outcome.reports;
else return [];
}),
};
}
return { type: "valid", value: value as { [K in keyof V]: Output<V[K]> } };
},
});
}
export function refine<V extends ValidatorSyncBase<any, any>>(originalValidator: V) {
return <const R>(refinementFunction: (value: Output<V>) => Array<R>) => {
return makeValidatorSync<Output<V>, Report<V> | R>({
validateSync(value) {
const originalOutcome = originalValidator.validateSync(value);
if (originalOutcome.type === "invalid") {
return { type: "invalid", reports: originalOutcome.reports };
} else {
const refinedOutcome = refinementFunction(originalOutcome.value);
if (refinedOutcome.length > 0) {
return { type: "invalid", reports: refinedOutcome };
} else {
return { type: "valid", value: originalOutcome.value };
}
}
},
});
};
}
export function union<V extends ValidatorSyncBase<any, any>[]>(originalValidators: V) {
return makeValidatorSync<Output<V[number]>, Report<V[number]>>({
validateSync(value) {
const outcomes = originalValidators.map((validator) => validator.validateSync(value));
if (outcomes.some((outcome) => outcome.type === "valid")) {
return { type: "valid", value: value as Output<V[number]> };
} else {
return {
type: "invalid",
reports: outcomes.flatMap((outcome) => {
if (outcome.type === "invalid") return outcome.reports;
else return [];
}),
};
}
},
});
}
export function intersection<
V1 extends ValidatorSyncBase<any, any>,
V2 extends ValidatorSyncBase<any, any> = V1,
V3 extends ValidatorSyncBase<any, any> = V1,
V4 extends ValidatorSyncBase<any, any> = V1,
>(
originalValidators: [V1, V2?, V3?, V4?],
): ValidatorSyncBase<
Output<V1> & Output<V2> & Output<V3> & Output<V4>,
Report<V1> | Report<V2> | Report<V3> | Report<V4>
> {
return {
validateSync(value) {
const outcomes = originalValidators.map((validator) => validator?.validateSync(value));
if (outcomes.some((outcome) => outcome?.type === "invalid")) {
return {
type: "invalid",
reports: outcomes.flatMap((outcome) => {
if (outcome?.type === "invalid") return outcome.reports;
else return [];
}),
};
} else {
return { type: "valid", value: value as Output<V1> & Output<V2> & Output<V3> & Output<V4> };
}
},
};
}
const u0 = undefined_(["REPORT"]);
type U0o = Output<typeof u0>;
type U0r = Report<typeof u0>;
const u0b = string([]);
type U0bo = Output<typeof u0b>;
type U0br = Report<typeof u0b>;
const u1 = object(
{
password: string([])
.refine((value) => (value.length === 0 ? [{ field: "password", label: "Password is required" }] : []))
.refine((value) => (value.length < 8 ? [{ field: "password", label: "Password too short" }] : [])),
confirmPassword: string([]),
},
[],
).refine((value) =>
value.password !== value.confirmPassword
? [
{ field: "confirmPassword", label: "Password does not match" },
{ field: "confirmPassword", label: "Password Must Match" },
]
: [],
);
type U1o = Output<typeof u1>;
type U1r = Report<typeof u1>;
const u2 = union([literal("a", ["A REPORT"]), literal("b", ["B REPORT"])]);
type U2o = Output<typeof u2>;
type U2r = Report<typeof u2>;
const u3 = union([
object({ type: literal("a", ["REPORT TYPE A"]), a: string([]) }, []),
object({ type: literal("b", []), b: number([]) }, []),
]);
type U3o = Output<typeof u3>;
type U3r = Report<typeof u3>;
const u4 = intersection([object({ a: string([]) }, []), object({ b: number([]) }, ["A", "B"])]);
type U4o = Output<typeof u4>;
type U4r = Report<typeof u4>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment