Skip to content

Commit

Permalink
feat: overhaul generics section of readme to include more details on …
Browse files Browse the repository at this point in the history
…z.ZodTypeAny usage (#3321)

* feat: overhaul generics section of readme to inclue more details on z.ZodTypeAny usage

* FMC

---------

Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
  • Loading branch information
braden-w and colinhacks authored Mar 14, 2024
1 parent 8910033 commit d63bde4
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 44 deletions.
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);;

0 comments on commit d63bde4

Please sign in to comment.