Type-safe parameter validation

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.