export type Reader = (v: unknown) => T | undefined; // An array passes `typeof v === "object"`, so it is rejected here. export function asObject(v: unknown): Record | undefined { return typeof v === "object" && v !== null && !Array.isArray(v) ? (v as Record) : undefined; } export const text: Reader = (v) => (typeof v === "string" ? v : undefined); export const filledText: Reader = (v) => (typeof v === "string" && v.length > 0 ? v : undefined); export const nonNegativeInteger: Reader = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : undefined; export const positiveInteger: Reader = (v) => typeof v === "number" && Number.isInteger(v) && v > 0 ? v : undefined; export const flag: Reader = (v) => (typeof v === "boolean" ? v : undefined); export function oneOf(values: readonly T[]): Reader { return (v) => values.find((member) => member === v); } /** A field with the wrong type is rejected rather than treated as "not sent", so a wrong-typed value cannot reach the screen. */ export function fieldsInto(body: Record, out: Partial) { return (key: K, read: Reader>): boolean => { const raw = body[key]; if (raw === undefined || raw === null) return true; const value = read(raw); if (value === undefined) return false; out[key] = value; return true; }; }