From 0d44b3cb942822be1a6fd78233ea08c7be8d79e3 Mon Sep 17 00:00:00 2001 From: Braden Wong <13159333+braden-w@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:05:26 -0700 Subject: [PATCH 1/2] feat: overhaul generics section of readme to inclue more details on z.ZodTypeAny usage --- README.md | 68 ++++++++++++++++++++++++++++++++++++++++++---- deno/lib/README.md | 68 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 124 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index fc0a4c61c..0c42bd705 100644 --- a/README.md +++ b/README.md @@ -2695,7 +2695,13 @@ type inferred = z.infer; // number ### Writing generic functions -When attempting to write a function that accepts a Zod schema as an input, it's common to try something like this: +By leveraging TypeScript's generic types, you can write reusable functions that accept Zod schemas as parameters. This enables you to create custom validation logic, schema transformations, and more, while maintaining type safety and inference. + +#### Limitations of `z.ZodType`, and why you should use `z.ZodTypeAny` instead + +When attempting to write a function that accepts a Zod schema as an input, it's tempting to use `z.ZodType` or `z.ZodSchema` as the type of the input schema so that `T` automatically represents the inferred type of the schema. However, this approach fails to narrow down the type subclass of the schema, which is especially important in cases where the function needs to return a schema that reflects the subclass of the input schema. + +Consider the following: ```ts function makeSchemaOptional(schema: z.ZodType) { @@ -2703,14 +2709,16 @@ function makeSchemaOptional(schema: z.ZodType) { } ``` -This approach has some issues. The `schema` variable in this function is typed as an instance of `ZodType`, which is an abstract class that all Zod schemas inherit from. This approach loses type information, namely _which subclass_ the input actually is. +In this function, even if the `schema` variable is defined as a more narrow subclass of `z.ZodType` (e.g. `z.ZodString`), `schema` is forcefully typed as an instance of `ZodType`, which is the broad abstract class that all Zod schemas inherit from, and `T` cannot be inferred as the more specific subclass. ```ts const arg = makeSchemaOptional(z.string()); -arg.unwrap(); +arg.unwrap(); // z.ZodType ``` -A better approach is for the generic parameter to refer to _the schema as a whole_. +This approach loses type information, namely _which subclass_ the input actually is. `arg.unwrap()` is typed as `z.ZodType` instead of the narrower and more specific subclass `z.ZodOptional`. + +A better approach is for the generic parameter to refer to _the schema as a whole_ using `z.ZodTypeAny`. ```ts function makeSchemaOptional(schema: T) { @@ -2720,13 +2728,61 @@ function makeSchemaOptional(schema: T) { > `ZodTypeAny` is just a shorthand for `ZodType`, a type that is broad enough to match any Zod schema. -As you can see, `schema` is now fully and properly typed. +Although at first glance, this seems less specific than the `z.ZodType`, it actually allows the type system to properly infer the narrowest possible type of `T`. ```ts const arg = makeSchemaOptional(z.string()); -arg.unwrap(); // ZodString +arg.unwrap(); // z.ZodOptional +``` + +By using `T extends z.ZodTypeAny`, the schema variable is now fully and properly typed, and the type system can infer the specific subclass of the schema. + +#### Using `z.infer` with `z.ZodTypeAny` to properly infer the parsed type + +If you follow the best practice of using `z.ZodTypeAny` as the generic parameter for your schema, you may encounter issues with the parsed data being typed as `any` instead of the inferred type of the schema. This can be especially frustrating if you used the `z.ZodType` approach before, as the data parsed by the schema were correctly inferred as `T`. + +```ts +function parseData(data: unknown, schema: T) { + const result = schema.safeParse(data); + if (!result.success) { + throw new Error('Validation failed'); + } + return result.data; +} + +const userSchema = z.object({ + name: z.string(), + age: z.number(), +}); + +const userData = parseData({ name: 'Alice', age: 25 }, userSchema); +// userData is incorrectly inferred as any ``` +To fix this, add a type assertion on the parsed value (currently typed as `any`) using `z.infer`. In our case with `parseData`, since `result.data` is typed as `any`, we can set the function's return type to `z.infer` to reflect the inferred type of the passed-in schema. + +```ts +function parseData(data: unknown, schema: T): z.infer { + const result = schema.safeParse(data); + if (!result.success) { + throw new Error('Validation failed'); + } + return result.data; +} + +const userSchema = z.object({ + name: z.string(), + age: z.number(), +}); + +const userData = parseData({ name: 'Alice', age: 25 }, userSchema); +// userData is correctly inferred as { name: string, age: number } +``` + +> If you do not use `z.infer`, TypeScript will infer the return type as `z.ZodTypeAny`, which is why the parsed data is typed as `any`. This is why it's important to use `z.infer` to properly infer the return type based on the passed-in schema. + +By following these best practices and leveraging z.infer, you can write generic functions that work seamlessly with Zod schemas while maintaining accurate type information throughout your codebase. + #### Constraining allowable inputs The `ZodType` class has three generic parameters. diff --git a/deno/lib/README.md b/deno/lib/README.md index b44d113ac..d346e3d5d 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -2694,7 +2694,13 @@ type inferred = z.infer; // number ### Writing generic functions -When attempting to write a function that accepts a Zod schema as an input, it's common to try something like this: +By leveraging TypeScript's generic types, you can write reusable functions that accept Zod schemas as parameters. This enables you to create custom validation logic, schema transformations, and more, while maintaining type safety and inference. + +#### Limitations of `z.ZodType`, and why you should use `z.ZodTypeAny` instead + +When attempting to write a function that accepts a Zod schema as an input, it's tempting to use `z.ZodType` or `z.ZodSchema` as the type of the input schema so that `T` automatically represents the inferred type of the schema. However, this approach fails to narrow down the type subclass of the schema, which is especially important in cases where the function needs to return a schema that reflects the subclass of the input schema. + +Consider the following: ```ts function makeSchemaOptional(schema: z.ZodType) { @@ -2702,14 +2708,16 @@ function makeSchemaOptional(schema: z.ZodType) { } ``` -This approach has some issues. The `schema` variable in this function is typed as an instance of `ZodType`, which is an abstract class that all Zod schemas inherit from. This approach loses type information, namely _which subclass_ the input actually is. +In this function, even if the `schema` variable is defined as a more narrow subclass of `z.ZodType` (e.g. `z.ZodString`), `schema` is forcefully typed as an instance of `ZodType`, which is the broad abstract class that all Zod schemas inherit from, and `T` cannot be inferred as the more specific subclass. ```ts const arg = makeSchemaOptional(z.string()); -arg.unwrap(); +arg.unwrap(); // z.ZodType ``` -A better approach is for the generic parameter to refer to _the schema as a whole_. +This approach loses type information, namely _which subclass_ the input actually is. `arg.unwrap()` is typed as `z.ZodType` instead of the narrower and more specific subclass `z.ZodOptional`. + +A better approach is for the generic parameter to refer to _the schema as a whole_ using `z.ZodTypeAny`. ```ts function makeSchemaOptional(schema: T) { @@ -2719,13 +2727,61 @@ function makeSchemaOptional(schema: T) { > `ZodTypeAny` is just a shorthand for `ZodType`, a type that is broad enough to match any Zod schema. -As you can see, `schema` is now fully and properly typed. +Although at first glance, this seems less specific than the `z.ZodType`, it actually allows the type system to properly infer the narrowest possible type of `T`. ```ts const arg = makeSchemaOptional(z.string()); -arg.unwrap(); // ZodString +arg.unwrap(); // z.ZodOptional +``` + +By using `T extends z.ZodTypeAny`, the schema variable is now fully and properly typed, and the type system can infer the specific subclass of the schema. + +#### Using `z.infer` with `z.ZodTypeAny` to properly infer the parsed type + +If you follow the best practice of using `z.ZodTypeAny` as the generic parameter for your schema, you may encounter issues with the parsed data being typed as `any` instead of the inferred type of the schema. This can be especially frustrating if you used the `z.ZodType` approach before, as the data parsed by the schema were correctly inferred as `T`. + +```ts +function parseData(data: unknown, schema: T) { + const result = schema.safeParse(data); + if (!result.success) { + throw new Error('Validation failed'); + } + return result.data; +} + +const userSchema = z.object({ + name: z.string(), + age: z.number(), +}); + +const userData = parseData({ name: 'Alice', age: 25 }, userSchema); +// userData is incorrectly inferred as any ``` +To fix this, add a type assertion on the parsed value (currently typed as `any`) using `z.infer`. In our case with `parseData`, since `result.data` is typed as `any`, we can set the function's return type to `z.infer` to reflect the inferred type of the passed-in schema. + +```ts +function parseData(data: unknown, schema: T): z.infer { + const result = schema.safeParse(data); + if (!result.success) { + throw new Error('Validation failed'); + } + return result.data; +} + +const userSchema = z.object({ + name: z.string(), + age: z.number(), +}); + +const userData = parseData({ name: 'Alice', age: 25 }, userSchema); +// userData is correctly inferred as { name: string, age: number } +``` + +> If you do not use `z.infer`, TypeScript will infer the return type as `z.ZodTypeAny`, which is why the parsed data is typed as `any`. This is why it's important to use `z.infer` to properly infer the return type based on the passed-in schema. + +By following these best practices and leveraging z.infer, you can write generic functions that work seamlessly with Zod schemas while maintaining accurate type information throughout your codebase. + #### Constraining allowable inputs The `ZodType` class has three generic parameters. From 48b4f05b925d22eb12a336d9981175d327e31e52 Mon Sep 17 00:00:00 2001 From: Colin McDonnell Date: Thu, 14 Mar 2024 15:24:10 -0700 Subject: [PATCH 2/2] FMC --- README.md | 81 +++++++++++++++------------------------------ deno/lib/README.md | 82 +++++++++++++++------------------------------- playground.ts | 20 ----------- 3 files changed, 53 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 0c42bd705..469ebb8ad 100644 --- a/README.md +++ b/README.md @@ -2695,94 +2695,65 @@ type inferred = z.infer; // number ### Writing generic functions -By leveraging TypeScript's generic types, you can write reusable functions that accept Zod schemas as parameters. This enables you to create custom validation logic, schema transformations, and more, while maintaining type safety and inference. +With TypeScript generics, you can write reusable functions that accept Zod schemas as parameters. This enables you to create custom validation logic, schema transformations, and more, while maintaining type safety and inference. -#### Limitations of `z.ZodType`, and why you should use `z.ZodTypeAny` instead - -When attempting to write a function that accepts a Zod schema as an input, it's tempting to use `z.ZodType` or `z.ZodSchema` as the type of the input schema so that `T` automatically represents the inferred type of the schema. However, this approach fails to narrow down the type subclass of the schema, which is especially important in cases where the function needs to return a schema that reflects the subclass of the input schema. - -Consider the following: +When attempting to write a function that accepts a Zod schema as an input, it's tempting to try something like this: ```ts -function makeSchemaOptional(schema: z.ZodType) { - return schema.optional(); +function inferSchema(schema: z.ZodType) { + return schema; } ``` -In this function, even if the `schema` variable is defined as a more narrow subclass of `z.ZodType` (e.g. `z.ZodString`), `schema` is forcefully typed as an instance of `ZodType`, which is the broad abstract class that all Zod schemas inherit from, and `T` cannot be inferred as the more specific subclass. +This approach is incorrect, and limits TypeScript's ability to properly infer the argument. No matter what you pass in, the type of `schema` will be an instance of `ZodType`. ```ts -const arg = makeSchemaOptional(z.string()); -arg.unwrap(); // z.ZodType +inferSchema(z.string()); +// => ZodType ``` -This approach loses type information, namely _which subclass_ the input actually is. `arg.unwrap()` is typed as `z.ZodType` instead of the narrower and more specific subclass `z.ZodOptional`. +This approach loses type information, namely _which subclass_ the input actually is (in this case, `ZodString`). That means you can't call any string-specific methods like `.min()` on the result of `inferSchema`. -A better approach is for the generic parameter to refer to _the schema as a whole_ using `z.ZodTypeAny`. +A better approach is to infer _the schema as a whole_ instead of merely it's inferred type. You can do this with a utility type called `z.ZodTypeAny`. ```ts -function makeSchemaOptional(schema: T) { - return schema.optional(); +function inferSchema(schema: T) { + return schema; } + +inferSchema(z.string()); +// => ZodString ``` > `ZodTypeAny` is just a shorthand for `ZodType`, a type that is broad enough to match any Zod schema. -Although at first glance, this seems less specific than the `z.ZodType`, it actually allows the type system to properly infer the narrowest possible type of `T`. +The Result is now fully and properly typed, and the type system can infer the specific subclass of the schema. -```ts -const arg = makeSchemaOptional(z.string()); -arg.unwrap(); // z.ZodOptional -``` - -By using `T extends z.ZodTypeAny`, the schema variable is now fully and properly typed, and the type system can infer the specific subclass of the schema. - -#### Using `z.infer` with `z.ZodTypeAny` to properly infer the parsed type +#### Inferring the inferred type -If you follow the best practice of using `z.ZodTypeAny` as the generic parameter for your schema, you may encounter issues with the parsed data being typed as `any` instead of the inferred type of the schema. This can be especially frustrating if you used the `z.ZodType` approach before, as the data parsed by the schema were correctly inferred as `T`. +If you follow the best practice of using `z.ZodTypeAny` as the generic parameter for your schema, you may encounter issues with the parsed data being typed as `any` instead of the inferred type of the schema. ```ts function parseData(data: unknown, schema: T) { - const result = schema.safeParse(data); - if (!result.success) { - throw new Error('Validation failed'); - } - return result.data; + return schema.parse(data); } -const userSchema = z.object({ - name: z.string(), - age: z.number(), -}); - -const userData = parseData({ name: 'Alice', age: 25 }, userSchema); -// userData is incorrectly inferred as any +parseData("sup", z.string()); +// => any ``` -To fix this, add a type assertion on the parsed value (currently typed as `any`) using `z.infer`. In our case with `parseData`, since `result.data` is typed as `any`, we can set the function's return type to `z.infer` to reflect the inferred type of the passed-in schema. +Due to how TypeScript inference works, it is treating `schema` like a `ZodTypeAny` instead of the inferred type. You can fix this with a type cast using `z.infer`. ```ts -function parseData(data: unknown, schema: T): z.infer { - const result = schema.safeParse(data); - if (!result.success) { - throw new Error('Validation failed'); - } - return result.data; +function parseData(data: unknown, schema: T) { + return schema.parse(data) as z.infer; + // ^^^^^^^^^^^^^^ <- add this } -const userSchema = z.object({ - name: z.string(), - age: z.number(), -}); - -const userData = parseData({ name: 'Alice', age: 25 }, userSchema); -// userData is correctly inferred as { name: string, age: number } +parseData("sup", z.string()); +// => string ``` -> If you do not use `z.infer`, TypeScript will infer the return type as `z.ZodTypeAny`, which is why the parsed data is typed as `any`. This is why it's important to use `z.infer` to properly infer the return type based on the passed-in schema. - -By following these best practices and leveraging z.infer, you can write generic functions that work seamlessly with Zod schemas while maintaining accurate type information throughout your codebase. - #### Constraining allowable inputs The `ZodType` class has three generic parameters. diff --git a/deno/lib/README.md b/deno/lib/README.md index d346e3d5d..469ebb8ad 100644 --- a/deno/lib/README.md +++ b/deno/lib/README.md @@ -774,6 +774,7 @@ z.coerce.boolean().parse(1); // => true z.coerce.boolean().parse([]); // => true z.coerce.boolean().parse(0); // => false +z.coerce.boolean().parse(""); // => false z.coerce.boolean().parse(undefined); // => false z.coerce.boolean().parse(null); // => false ``` @@ -2694,94 +2695,65 @@ type inferred = z.infer; // number ### Writing generic functions -By leveraging TypeScript's generic types, you can write reusable functions that accept Zod schemas as parameters. This enables you to create custom validation logic, schema transformations, and more, while maintaining type safety and inference. +With TypeScript generics, you can write reusable functions that accept Zod schemas as parameters. This enables you to create custom validation logic, schema transformations, and more, while maintaining type safety and inference. -#### Limitations of `z.ZodType`, and why you should use `z.ZodTypeAny` instead - -When attempting to write a function that accepts a Zod schema as an input, it's tempting to use `z.ZodType` or `z.ZodSchema` as the type of the input schema so that `T` automatically represents the inferred type of the schema. However, this approach fails to narrow down the type subclass of the schema, which is especially important in cases where the function needs to return a schema that reflects the subclass of the input schema. - -Consider the following: +When attempting to write a function that accepts a Zod schema as an input, it's tempting to try something like this: ```ts -function makeSchemaOptional(schema: z.ZodType) { - return schema.optional(); +function inferSchema(schema: z.ZodType) { + return schema; } ``` -In this function, even if the `schema` variable is defined as a more narrow subclass of `z.ZodType` (e.g. `z.ZodString`), `schema` is forcefully typed as an instance of `ZodType`, which is the broad abstract class that all Zod schemas inherit from, and `T` cannot be inferred as the more specific subclass. +This approach is incorrect, and limits TypeScript's ability to properly infer the argument. No matter what you pass in, the type of `schema` will be an instance of `ZodType`. ```ts -const arg = makeSchemaOptional(z.string()); -arg.unwrap(); // z.ZodType +inferSchema(z.string()); +// => ZodType ``` -This approach loses type information, namely _which subclass_ the input actually is. `arg.unwrap()` is typed as `z.ZodType` instead of the narrower and more specific subclass `z.ZodOptional`. +This approach loses type information, namely _which subclass_ the input actually is (in this case, `ZodString`). That means you can't call any string-specific methods like `.min()` on the result of `inferSchema`. -A better approach is for the generic parameter to refer to _the schema as a whole_ using `z.ZodTypeAny`. +A better approach is to infer _the schema as a whole_ instead of merely it's inferred type. You can do this with a utility type called `z.ZodTypeAny`. ```ts -function makeSchemaOptional(schema: T) { - return schema.optional(); +function inferSchema(schema: T) { + return schema; } + +inferSchema(z.string()); +// => ZodString ``` > `ZodTypeAny` is just a shorthand for `ZodType`, a type that is broad enough to match any Zod schema. -Although at first glance, this seems less specific than the `z.ZodType`, it actually allows the type system to properly infer the narrowest possible type of `T`. +The Result is now fully and properly typed, and the type system can infer the specific subclass of the schema. -```ts -const arg = makeSchemaOptional(z.string()); -arg.unwrap(); // z.ZodOptional -``` - -By using `T extends z.ZodTypeAny`, the schema variable is now fully and properly typed, and the type system can infer the specific subclass of the schema. - -#### Using `z.infer` with `z.ZodTypeAny` to properly infer the parsed type +#### Inferring the inferred type -If you follow the best practice of using `z.ZodTypeAny` as the generic parameter for your schema, you may encounter issues with the parsed data being typed as `any` instead of the inferred type of the schema. This can be especially frustrating if you used the `z.ZodType` approach before, as the data parsed by the schema were correctly inferred as `T`. +If you follow the best practice of using `z.ZodTypeAny` as the generic parameter for your schema, you may encounter issues with the parsed data being typed as `any` instead of the inferred type of the schema. ```ts function parseData(data: unknown, schema: T) { - const result = schema.safeParse(data); - if (!result.success) { - throw new Error('Validation failed'); - } - return result.data; + return schema.parse(data); } -const userSchema = z.object({ - name: z.string(), - age: z.number(), -}); - -const userData = parseData({ name: 'Alice', age: 25 }, userSchema); -// userData is incorrectly inferred as any +parseData("sup", z.string()); +// => any ``` -To fix this, add a type assertion on the parsed value (currently typed as `any`) using `z.infer`. In our case with `parseData`, since `result.data` is typed as `any`, we can set the function's return type to `z.infer` to reflect the inferred type of the passed-in schema. +Due to how TypeScript inference works, it is treating `schema` like a `ZodTypeAny` instead of the inferred type. You can fix this with a type cast using `z.infer`. ```ts -function parseData(data: unknown, schema: T): z.infer { - const result = schema.safeParse(data); - if (!result.success) { - throw new Error('Validation failed'); - } - return result.data; +function parseData(data: unknown, schema: T) { + return schema.parse(data) as z.infer; + // ^^^^^^^^^^^^^^ <- add this } -const userSchema = z.object({ - name: z.string(), - age: z.number(), -}); - -const userData = parseData({ name: 'Alice', age: 25 }, userSchema); -// userData is correctly inferred as { name: string, age: number } +parseData("sup", z.string()); +// => string ``` -> If you do not use `z.infer`, TypeScript will infer the return type as `z.ZodTypeAny`, which is why the parsed data is typed as `any`. This is why it's important to use `z.infer` to properly infer the return type based on the passed-in schema. - -By following these best practices and leveraging z.infer, you can write generic functions that work seamlessly with Zod schemas while maintaining accurate type information throughout your codebase. - #### Constraining allowable inputs The `ZodType` class has three generic parameters. diff --git a/playground.ts b/playground.ts index 2d73515e0..4e01473b6 100644 --- a/playground.ts +++ b/playground.ts @@ -1,23 +1,3 @@ - import { z } from "./src"; z; - -// const enumErrorMap = { -// errorMap: () => ({ invalid_type_error: "Error Message" }), -// }; - - -// const TestEnum = z.enum(['value1', 'value2'], -const TestEnum = z.nativeEnum({"value1":1, "value2":2}, - { - invalid_type_error: "Invalid type", - required_error: "Required value", - // errorMap: () => ({ message: "Custom error message" }), - - } -); - -console.log(TestEnum.safeParse('3').error);; // Custom error message -console.log(TestEnum.safeParse(undefined).error);; -