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

feat: overhaul generics section of readme to include more details on z.ZodTypeAny usage #3321

Merged
merged 2 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 39 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2695,36 +2695,63 @@ type inferred = z.infer<typeof stringToNumber>; // 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:
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.

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<T>(schema: z.ZodType<T>) {
return schema.optional();
function inferSchema<T>(schema: z.ZodType<T>) {
return schema;
}
```

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.
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();
inferSchema(z.string());
// => ZodType<string>
```

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 (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 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<T extends z.ZodTypeAny>(schema: T) {
return schema.optional();
function inferSchema<T extends z.ZodTypeAny>(schema: T) {
return schema;
}

inferSchema(z.string());
// => ZodString
```

> `ZodTypeAny` is just a shorthand for `ZodType<any, any, any>`, a type that is broad enough to match any Zod schema.

As you can see, `schema` is now fully and properly typed.
The Result is now fully and properly typed, and the type system can infer the specific subclass of the schema.

#### 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.

```ts
const arg = makeSchemaOptional(z.string());
arg.unwrap(); // ZodString
function parseData<T extends z.ZodTypeAny>(data: unknown, schema: T) {
return schema.parse(data);
}

parseData("sup", z.string());
// => any
```

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<T extends z.ZodTypeAny>(data: unknown, schema: T) {
return schema.parse(data) as z.infer<T>;
// ^^^^^^^^^^^^^^ <- add this
}

parseData("sup", z.string());
// => string
```

#### Constraining allowable inputs
Expand Down
52 changes: 40 additions & 12 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -2694,36 +2695,63 @@ type inferred = z.infer<typeof stringToNumber>; // 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:
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.

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<T>(schema: z.ZodType<T>) {
return schema.optional();
function inferSchema<T>(schema: z.ZodType<T>) {
return schema;
}
```

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.
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();
inferSchema(z.string());
// => ZodType<string>
```

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 (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 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<T extends z.ZodTypeAny>(schema: T) {
return schema.optional();
function inferSchema<T extends z.ZodTypeAny>(schema: T) {
return schema;
}

inferSchema(z.string());
// => ZodString
```

> `ZodTypeAny` is just a shorthand for `ZodType<any, any, any>`, a type that is broad enough to match any Zod schema.

As you can see, `schema` is now fully and properly typed.
The Result is now fully and properly typed, and the type system can infer the specific subclass of the schema.

#### 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.

```ts
const arg = makeSchemaOptional(z.string());
arg.unwrap(); // ZodString
function parseData<T extends z.ZodTypeAny>(data: unknown, schema: T) {
return schema.parse(data);
}

parseData("sup", z.string());
// => any
```

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<T extends z.ZodTypeAny>(data: unknown, schema: T) {
return schema.parse(data) as z.infer<T>;
// ^^^^^^^^^^^^^^ <- add this
}

parseData("sup", z.string());
// => string
```

#### Constraining allowable inputs
Expand Down
20 changes: 0 additions & 20 deletions playground.ts
Original file line number Diff line number Diff line change
@@ -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);;

Loading