/** * Represents a value we have obtained from parsing JSON which we know is an object, * and expect to be of some type `T` which has not yet been validated. */ export type UnvalidatedObject = { [P in keyof T]?: unknown }; /** Represents a value we have obtained from parsing JSON which we know is an array. */ export type UnvalidatedArray = unknown[]; /** * Attempts to parse `data` as JSON. This function does not perform any validation and will therefore * return a value of an `unknown` type if successful. Throws if `data` is not valid JSON. */ export function parseString(data: string): unknown { return JSON.parse(data) as unknown; } /** Asserts that `value` is an object, which is not yet validated, but expected to be of type `T`. */ export function isObject(value: unknown): value is UnvalidatedObject { return typeof value === "object" && value !== null && !Array.isArray(value); } /** Asserts that `value` is an array, which is not yet validated. */ export function isArray(value: unknown): value is UnvalidatedArray { return Array.isArray(value); } /** Asserts that `value` is a string. */ export function isString(value: unknown): value is string { return typeof value === "string"; } /** Asserts that `value` is either a string or undefined. */ export function isStringOrUndefined( value: unknown, ): value is string | undefined { return value === undefined || isString(value); } /** * Represents a field of type `T` in a schema. * Carries a validation function and flag indicating whether the field is required or not. */ export type Validator = { validate: (val: unknown) => val is T; required: boolean; }; /** Extracts `T` from `Validator`. */ export type UnwrapValidator = V extends Validator ? A : never; /** A validator for string fields in schemas. */ export const string = { validate: isString, required: true, } as const satisfies Validator; /** Transforms a validator to be optional. */ export function optional(validator: Validator) { return { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); }, required: false, } as const satisfies Validator; } /** Represents an arbitrary object schema. */ export type Schema = Record>; /** Extracts the required keys from `S`. */ export type RequiredKeys = { [K in keyof S]: S[K]["required"] extends true ? K : never; }[keyof S]; /** Extracts optional keys from `S`. */ export type OptionalKeys = { [K in keyof S]: S[K]["required"] extends true ? never : K; }[keyof S]; /** Constructs an object type corresponding to a schema. */ export type FromSchema = { [K in RequiredKeys]: UnwrapValidator; } & { [K in OptionalKeys]?: UnwrapValidator }; /** * Validates that `obj` satisfies at least `schema`. Additional keys are accepted. * * @param schema The schema to validate against. * @param obj The object to validate. * @returns Asserts that `obj` is of the `schema`'s type if validation is successful. */ export function validateSchema( schema: S, obj: UnvalidatedObject, ): obj is FromSchema { for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; // If the property is required, but absent, fail. if (validator.required && !hasKey) { return false; } // If the property is required, but undefined or null, fail. if (validator.required && (obj[key] === undefined || obj[key] === null)) { return false; } // If the property is present, validate it. if (hasKey && !validator.validate(obj[key])) { return false; } } return true; }