Skip to content

Commit

Permalink
Update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
colinhacks committed Mar 14, 2024
1 parent e6d0839 commit a7acdd1
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 111 deletions.
81 changes: 26 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2694,94 +2694,65 @@ type inferred = z.infer<typeof stringToNumber>; // 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<T>`, 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<T>` or `z.ZodSchema<T>` 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<T>(schema: z.ZodType<T>) {
return schema.optional();
function inferSchema<T>(schema: z.ZodType<T>) {
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<string, z.ZodTypeDef, string>
inferSchema(z.string());
// => ZodType<string>
```

This approach loses type information, namely _which subclass_ the input actually is. `arg.unwrap()` is typed as `z.ZodType<string, z.ZodTypeDef, string>` instead of the narrower and more specific subclass `z.ZodOptional<z.ZodString>`.
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<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.
Although at first glance, this seems less specific than the `z.ZodType<T>`, 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<z.ZodString>
```

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<T>` 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<T extends z.ZodTypeAny>(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<T>`. In our case with `parseData`, since `result.data` is typed as `any`, we can set the function's return type to `z.infer<T>` 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<T extends z.ZodTypeAny>(data: unknown, schema: T): z.infer<T> {
const result = schema.safeParse(data);
if (!result.success) {
throw new Error('Validation failed');
}
return result.data;
function parseData<T extends z.ZodTypeAny>(data: unknown, schema: T) {
return schema.parse(data) as z.infer<T>;
// ^^^^^^^^^^^^^^ <- 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.
Expand Down
81 changes: 26 additions & 55 deletions deno/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2694,94 +2694,65 @@ type inferred = z.infer<typeof stringToNumber>; // 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<T>`, 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<T>` or `z.ZodSchema<T>` 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<T>(schema: z.ZodType<T>) {
return schema.optional();
function inferSchema<T>(schema: z.ZodType<T>) {
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<string, z.ZodTypeDef, string>
inferSchema(z.string());
// => ZodType<string>
```

This approach loses type information, namely _which subclass_ the input actually is. `arg.unwrap()` is typed as `z.ZodType<string, z.ZodTypeDef, string>` instead of the narrower and more specific subclass `z.ZodOptional<z.ZodString>`.
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<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.
Although at first glance, this seems less specific than the `z.ZodType<T>`, 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<z.ZodString>
```

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<T>` 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<T extends z.ZodTypeAny>(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<T>`. In our case with `parseData`, since `result.data` is typed as `any`, we can set the function's return type to `z.infer<T>` 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<T extends z.ZodTypeAny>(data: unknown, schema: T): z.infer<T> {
const result = schema.safeParse(data);
if (!result.success) {
throw new Error('Validation failed');
}
return result.data;
function parseData<T extends z.ZodTypeAny>(data: unknown, schema: T) {
return schema.parse(data) as z.infer<T>;
// ^^^^^^^^^^^^^^ <- 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.
Expand Down
2 changes: 1 addition & 1 deletion playground.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { z } from "./src";

z;
z;

0 comments on commit a7acdd1

Please sign in to comment.