summary#
implement a minimal validation helper that leverages typescript’s powerful type system for type inference and runtime safety
shortcoming#
currently, ctx.body.json<T>() is effectively just a shorthand for a mere type assertion (as T). it doesn't verify the shape of the data at runtime, forcing users to either trust client input blindly (security risk) or import heavy libraries like zod. for a framework this size, pulling in a ≥50kb validator just to check an email format or a string length feels bloated and inefficient
proposed solution#
introduce a schema primitive that leverages as const assertions and conditional types to infer types from the runtime definition, effectively making the schema definition the source of truth for both the compiler and the runtime validator or smth like that
vnamespace (?): it’s a common pattern among validation libraries to have a vaguely named primitive exporting everything. this name is subject to change. for now, i guessvas short forvalidatoris descriptive enough- implement a mapped type that iterates over the schema object. if a key maps to
v.string(), it resolves to string. if it maps to another object schema, it recurses. this eliminates the need to manually define interfaces;typeof schemabecomes the interface itself - the runtime implementation should be a tiny, recursive traversal function leveraging basic format validation. kind of acts like a single-pass runtime or a lightweight guard
- use template literal types (e.g.
${string}@${string}.${string}) or branded types (string & { readonly __brand: unique symbol }) for specific formats like email - the system should allow developers to define custom types (both compile-time and runtime) easily, extending the
vnamespace for their specific needs
proof of concept#
type SchemaDefinition<T = any> = {
check: (val: unknown) => boolean;
__type: T;
};
// helper type to infer the output type from the schema :3
type InferSchema<T> = T extends SchemaDefinition<infer U> ? U
: T extends Record<string, SchemaDefinition<any>> ? { [K in keyof T]: InferSchema<T[K]> }
: never;
type Email = `${string}@${string}.${string}` & { readonly __brand: unique symbol };
export function v<const T extends Record<string, SchemaDefinition<any>>>(
shape: T,
): SchemaDefinition<InferSchema<typeof shape>> {
return {
check: (val) => {
if (typeof val !== "object" || val === null) return false;
for (const key in shape) {
if (!shape[key].check((val as any)[key])) return false;
}
return true;
},
__type: null as any,
};
}
v.string = (): SchemaDefinition<string> => ({
check: (v) => typeof v === "string",
__type: "" as string,
});
v.number = (): SchemaDefinition<number> => ({
check: (v) => typeof v === "number",
__type: 0 as number,
});
v.email = (): SchemaDefinition<Email> => ({
check: (v) => typeof v === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
__type: "" as Email,
});
v.custom = <T>(fn: (val: unknown) => val is T): SchemaDefinition<T> => ({
check: fn,
__type: null as any,
});
export const validate = <const T extends SchemaDefinition<any>>(
schema: T,
data: unknown,
): InferSchema<typeof schema> => {
if (!schema.check(data)) {
throw new Error("validation failed");
}
return data as InferSchema<typeof schema>;
};
const _userSchema = v({
username: v.string(),
age: v.number(),
contact: v.email(),
});