tl;dr see it in action: https://github.com/scarabcoder/function-factory
With the release of the Standard Schema interface, it’s gotten a lot easier to implement validator-agnostic libraries that can take in a schema from many popular schema libraries.
Using the new Standard Schema interface, I created a tiny helper function that can construct functions which automatically validates the input, applying any transformations from the schema, all in a type-safe way.
The whole library exists as this one function:
export function makeFunction<TSchema extends StandardSchemaV1, TResult>(
schema: TSchema,
impl: (parsed: StandardSchemaV1.InferOutput<TSchema>) => TResult
) {
return (params: StandardSchemaV1.InferInput<TSchema>): TResult => {
const result = schema["~standard"].validate(params);
if("then" in result) {
throw new Error("Promises are not supported in results.")
}
if(result.issues) {
// Just a custom error that can take in the error array
throw new InvalidParametersError(result.issues);
}
return impl(result.value);
};
}
Because it implements the Standard Schema, you can can use it with any schema library that implements the interface, for example with Zod:
const todoSchema = z.object({
taskName: z.string().min(1, 'Task name is required'),
completed: z.boolean().default(false),
description: z.string().optional().nullable().default(null)
});
const makeTodo = makeFunction(todoSchema, (task) => {
console.log(`Created task: ${task.taskName}, Completed: ${task.completed}, Description: ${task.description}`);
});
makeTodo({
taskName: 'Task with a default null description',
completed: true
});
// console: Created task: Task with a default null description, Completed: true, Description: null
Or with Valibot:
const todoSchema = object({
taskName: pipe(
string(),
minLength(1, 'Task name is required'),
),
completed: optional(boolean(), false),
description: optional(nullable(string()), null),
});
const makeTodo = makeFunction(todoSchema, (task) => {
console.log(
`Created task: ${task.taskName}, Completed: ${task.completed}, Description: ${task.description}`,
);
});
makeTodo({
taskName: 'Task with a default null description',
completed: true
});
// console: Created task: Task with a default null description, Completed: true, Description: null
This allows functions to be written with guaranteed runtime type safety, plus transforms such as defaults. By writing schemas that are more specific (such as string -> UUID) you can get more descriptive error messages if something ever does go wrong.