Skip to content

[v4] catch shouldn't widen its input type #4851

@niba

Description

@niba

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions