Created
May 22, 2024 00:16
-
-
Save freddi301/6dfa48b7473926f933d31c8e9d766236 to your computer and use it in GitHub Desktop.
typescript validation library experiment
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
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