Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC/Feature Request: Add a type-safe convert/input output mapper method for compile time input validation #3860

Open
oberbeck opened this issue Nov 19, 2024 · 1 comment · May be fixed by #3861

Comments

@oberbeck
Copy link

oberbeck commented Nov 19, 2024

RFC/Feature Request: Add a type-safe convert/input output mapper method for compile time input validation

Summary

Introduce a new method, ie. convert, for Zod schemas that provides type-safe input validation and transformation. This method would enforce that the provided input matches the schema’s defined input type (T["_input"]) at compile time, ensuring a stricter contract compared to parse.

Motivation

Zod’s parse and safeParse methods currently accept unknown as input. While this is intentional for unstructured or untrusted data validation, there are common use cases where input data is already structured (e.g., from a typed API response, database query, or other controlled sources). For such cases, allowing type-safe input validation without relying on runtime type checking enhances both safety and developer experience.

The proposed method aligns with Zod’s TypeScript-first philosophy by:

  1. Enforcing input types at compile time, reducing reliance on runtime checks.
  2. Providing a clear distinction between structured and unstructured input handling.
  3. Supporting scenarios where transformations (via .transform()) are applied, while maintaining type safety.

Proposed API

Add a method convert to Zod schemas with the following behavior:

schema.convert(input: T["_input"]): T["_output"];
  • Input Type (T["_input"]): Enforces that the input matches the schema's input type at compile time.

  • Output Type (T["_output"]): Returns the validated and transformed data, adhering to the schema's transformation logic.

    Example Usage

test("valid schema conversion", () => {
const userSchema = z.object({
id: z.string(),
name: z.string(),
age: z.string().transform((age) => parseInt(age, 10)),
});
const validInput: z.input<typeof userSchema> = {
id: "123",
name: "Alice",
age: "25",
};
const user = userSchema.convert(validInput);
expectTypeOf(user).toMatchTypeOf<z.infer<typeof userSchema>>();
});
test("invalid schema conversion", () => {
const userSchema = z.object({
id: z.string(),
name: z.string(),
age: z.string().transform((age) => parseInt(age, 10)),
});
// Input not matching the schema's input type
const invalidInput = {
name: "Alice",
age: "25",
};
expectTypeOf(numberToString.convert)
.parameter(0)
.not.toMatchTypeOf<typeof invalidInput>();
try {
// @ts-expect-error - compile error
userSchema.convert(invalidInput);
} catch {}
});

Comparison with Existing Methods

  1. parse:

    • Accepts unknown input and validates it.
    • Use case: General-purpose validation of unstructured data.
  2. convert (Proposed):

    • Enforces that the input matches the schema’s input type at compile time.
    • Use case:
      • Validating already-structured data with guaranteed input types, while benefiting from transformations and Zod’s validation.
      • Using zod as typesafe converter ie. between well defined input and output types.

Potential Concerns

  • Confusion with Existing Methods:
    • I am uncertain about how to name the method in order to clearly differentiate itself from parse, safeParse, transform,... to avoid ambiguity. Suggested name: convert, inputToOutput, parseConvert?

Example implementation

#3861

zod/src/types.ts

Lines 521 to 541 in dad8820

convert(data: Input, params?: Partial<ParseParams>): Output {
return this.parse(data, params);
}
safeConvert(
data: Input,
params?: Partial<ParseParams>
): SafeParseReturnType<Input, Output> {
return this.safeParse(data, params);
}
convertAsync(data: Input, params?: Partial<ParseParams>): Promise<Output> {
return this.parseAsync(data, params);
}
safeConvertAsync(
data: Input,
params?: Partial<ParseParams>
): Promise<SafeParseReturnType<Input, Output>> {
return this.safeParseAsync(data, params);
}

@oberbeck oberbeck linked a pull request Nov 19, 2024 that will close this issue
@oberbeck oberbeck changed the title RFC/Feature Request: Add a type-safe convert/input output mapper method for compile type input validation RFC/Feature Request: Add a type-safe convert/input output mapper method for compile time input validation Nov 19, 2024
@redbmk
Copy link

redbmk commented Dec 5, 2024

This would be awesome!

Another option instead of adding a new function would be to add a param to ParseParams that would enforce input type-checking. Something like:

export declare type ParseParams = {
  path: (string | number)[];
  errorMap: ZodErrorMap;
  async: boolean;
  typedInput: boolean; // this would be new
}

class ZodType {
  // ...

  // add definition overrides
  parse(data: unknown, params?: Partial<ParseParams>): Output
  parse(data: Input, params: Partial<Omit<ParseParams, "typedInput">> & { typedInput: true }): Output

  // current implementation wouldn't need to change
  parse(data: unknown, params?: Partial<ParseParams>): Output {
    const result = this.safeParse(data, params);
    if (result.success) return result.data;
    throw result.error;
  }
}

Then you could call it with:

z.object({ name: z.string() }).parse({}); // runtime error
z.object({ name: z.string() }).parse({}, { typedInput: true }); // compile-time and runtime error

Not exactly sold on the name typedInput, however... maybe strict: true?

I do kinda think I like the idea of a separate function better though. What about the name strictParse, or would that get confused with .strict()? Maybe parseKnown?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants