-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Developers commonly use catch
as a fallback mechanism for handling external input. The goal is to guarantee that a valid value is set, even if the external input is incorrect. This usage pattern is common and can be found in libraries like TanStack Router.
The problem with the current implementation of catch
is that it widens the schema's inferred input type to accept Whatever
.
const schema = z.object({
page: z.number().default(1),
filter: z.string().default(''),
sort: z.enum(['newest', 'oldest', 'price']).default('newest').catch("newest"),
})
/*
Zod 3 output: {
page?: number | undefined;
filter?: string | undefined;
sort?: unknown; // catch causes unknown type in zod v3
}
/*
Zod 4 output: {
page?: number | undefined;
filter?: string | undefined;
sort?: z4.core.util.Whatever | "newest" | "oldest" | "price";
} */
type Zod4Type = StandardSchemaV1.InferInput<typeof schema>
Because the input type now includes Whatever
, it's possible to pass any value to it (e.g., sort="test"
) without a compilation error. This also becomes a problem during refactoring, as it hides potential bugs when enum values are changed or removed.
The intent of using catch
isn't to signal that any value is acceptable. Instead, the intent is to handle invalid external data gracefully while maintaining strict type safety for internal code. Developers within the codebase should not be encouraged to pass arbitrary values.
Other validation libraries, like Valibot and ArkType, generate a strict input type for their catch
(or equivalent) functionality. Only Zod appears to widen the type in this way.
Linked issues / pr: TanStack/router#4322, TanStack/router#4442