zod
Version:
TypeScript-first schema declaration and validation library with static type inference
403 lines (322 loc) • 13.3 kB
text/typescript
import { expectTypeOf, test } from "vitest";
import * as z from "zod/v4";
import * as core from "zod/v4/core";
test("assignability", () => {
// $ZodString
z.string() satisfies z.core.$ZodString;
z.string() satisfies z.ZodString;
// $ZodNumber
z.number() satisfies z.core.$ZodNumber;
z.number() satisfies z.ZodNumber;
// $ZodBigInt
z.bigint() satisfies z.core.$ZodBigInt;
z.bigint() satisfies z.ZodBigInt;
// $ZodBoolean
z.boolean() satisfies z.core.$ZodBoolean;
z.boolean() satisfies z.ZodBoolean;
// $ZodDate
z.date() satisfies z.core.$ZodDate;
z.date() satisfies z.ZodDate;
// $ZodSymbol
z.symbol() satisfies z.core.$ZodSymbol;
z.symbol() satisfies z.ZodSymbol;
// $ZodUndefined
z.undefined() satisfies z.core.$ZodUndefined;
z.undefined() satisfies z.ZodUndefined;
// $ZodNullable
z.string().nullable() satisfies z.core.$ZodNullable;
z.string().nullable() satisfies z.ZodNullable;
// $ZodNull
z.null() satisfies z.core.$ZodNull;
z.null() satisfies z.ZodNull;
// $ZodAny
z.any() satisfies z.core.$ZodAny;
z.any() satisfies z.ZodAny;
// $ZodUnknown
z.unknown() satisfies z.core.$ZodUnknown;
z.unknown() satisfies z.ZodUnknown;
// $ZodNever
z.never() satisfies z.core.$ZodNever;
z.never() satisfies z.ZodNever;
// $ZodVoid
z.void() satisfies z.core.$ZodVoid;
z.void() satisfies z.ZodVoid;
// $ZodArray
z.array(z.string()) satisfies z.core.$ZodArray;
z.array(z.string()) satisfies z.ZodArray;
z.array(z.string()) satisfies z.ZodType<Array<unknown>>;
// $ZodObject
z.object({ key: z.string() }) satisfies z.core.$ZodObject;
z.object({ key: z.string() }) satisfies z.ZodObject<{ key: z.ZodType }>;
z.object({ key: z.string() }) satisfies z.ZodType<{ key: string }>;
// $ZodUnion
z.union([z.string(), z.number()]) satisfies z.core.$ZodUnion;
z.union([z.string(), z.number()]) satisfies z.ZodUnion;
z.union([z.string(), z.number()]) satisfies z.ZodType<string | number>;
// $ZodIntersection
z.intersection(z.string(), z.number()) satisfies z.core.$ZodIntersection;
z.intersection(z.string(), z.number()) satisfies z.ZodIntersection;
// $ZodTuple
z.tuple([z.string(), z.number()]) satisfies z.core.$ZodTuple;
z.tuple([z.string(), z.number()]) satisfies z.ZodTuple;
// $ZodRecord
z.record(z.string(), z.number()) satisfies z.core.$ZodRecord;
z.record(z.string(), z.number()) satisfies z.ZodRecord;
// $ZodMap
z.map(z.string(), z.number()) satisfies z.core.$ZodMap;
z.map(z.string(), z.number()) satisfies z.ZodMap;
// $ZodSet
z.set(z.string()) satisfies z.core.$ZodSet;
z.set(z.string()) satisfies z.ZodSet;
// $ZodLiteral
z.literal("example") satisfies z.core.$ZodLiteral;
z.literal("example") satisfies z.ZodLiteral;
// $ZodEnum
z.enum(["a", "b", "c"]) satisfies z.core.$ZodEnum;
z.enum(["a", "b", "c"]) satisfies z.ZodEnum;
// $ZodPromise
z.promise(z.string()) satisfies z.core.$ZodPromise;
z.promise(z.string()) satisfies z.ZodPromise;
// $ZodLazy
const lazySchema = z.lazy(() => z.string());
lazySchema satisfies z.core.$ZodLazy;
lazySchema satisfies z.ZodLazy;
// $ZodOptional
z.string().optional() satisfies z.core.$ZodOptional;
z.string().optional() satisfies z.ZodOptional;
// $ZodDefault
z.string().default("default") satisfies z.core.$ZodDefault;
z.string().default("default") satisfies z.ZodDefault;
// $ZodTemplateLiteral
z.templateLiteral([z.literal("a"), z.literal("b")]) satisfies z.core.$ZodTemplateLiteral;
z.templateLiteral([z.literal("a"), z.literal("b")]) satisfies z.ZodTemplateLiteral;
// $ZodCustom
z.custom<string>((val) => typeof val === "string") satisfies z.core.$ZodCustom;
z.custom<string>((val) => typeof val === "string") satisfies z.ZodCustom;
// $ZodTransform
z.transform((val) => val as string) satisfies z.core.$ZodTransform;
z.transform((val) => val as string) satisfies z.ZodTransform;
// $ZodNonOptional
z.string().optional().nonoptional() satisfies z.core.$ZodNonOptional;
z.string().optional().nonoptional() satisfies z.ZodNonOptional;
// $ZodReadonly
z.object({ key: z.string() }).readonly() satisfies z.core.$ZodReadonly;
z.object({ key: z.string() }).readonly() satisfies z.ZodReadonly;
// $ZodNaN
z.nan() satisfies z.core.$ZodNaN;
z.nan() satisfies z.ZodNaN;
// $ZodPipe
z.unknown().pipe(z.number()) satisfies z.core.$ZodPipe;
z.unknown().pipe(z.number()) satisfies z.ZodPipe;
// $ZodPreprocess
z.preprocess((v) => v, z.number()) satisfies z.core.$ZodPreprocess;
z.preprocess((v) => v, z.number()) satisfies z.ZodPreprocess;
z.preprocess((v) => v, z.number()) satisfies z.core.$ZodPipe<z.core.$ZodTransform, z.ZodNumber>;
z.preprocess((v) => v, z.number()) satisfies z.ZodPipe<z.core.$ZodTransform, z.ZodNumber>;
// $ZodSuccess
z.success(z.string()) satisfies z.core.$ZodSuccess;
z.success(z.string()) satisfies z.ZodSuccess;
// $ZodCatch
z.string().catch("fallback") satisfies z.core.$ZodCatch;
z.string().catch("fallback") satisfies z.ZodCatch;
// $ZodFile
z.file() satisfies z.core.$ZodFile;
z.file() satisfies z.ZodFile;
});
test("toZod", () => {
type Company = {
id: string;
name: string;
webAddress: string | null;
};
const CompanySchema = z.object({
id: z.string(),
name: z.string(),
webAddress: z.string().url().nullable(),
});
const Company = z.toZod<Company>()(CompanySchema);
Company.shape.id.min(1);
expectTypeOf<z.output<typeof Company>>().toEqualTypeOf<Company>();
const CoreCompany = core.toZod<Company>()(CompanySchema);
expectTypeOf<core.output<typeof CoreCompany>>().toEqualTypeOf<Company>();
const UtilCompany = z.core.util.toZod<Company>()(CompanySchema);
expectTypeOf<z.output<typeof UtilCompany>>().toEqualTypeOf<Company>();
type Complex = {
kind: "company";
nested: {
founded: Date;
tags: string[];
};
scores: Record<"a" | "b", number>;
tuple: [string, number];
status: "active" | "inactive";
};
const Complex = z.toZod<Complex>()(
z.object({
kind: z.literal("company"),
nested: z.object({
founded: z.date(),
tags: z.array(z.string()),
}),
scores: z.record(z.enum(["a", "b"]), z.number()),
tuple: z.tuple([z.string(), z.number()]),
status: z.union([z.literal("active"), z.literal("inactive")]),
})
);
expectTypeOf<z.output<typeof Complex>>().toEqualTypeOf<Complex>();
type Items = {
items: { value: string }[];
};
const Items = z.toZod<Items>()(
z.object({
items: z.array(z.object({ value: z.string() })),
})
);
expectTypeOf<z.output<typeof Items>>().toEqualTypeOf<Items>();
type ReadonlyItem = Readonly<{
id: string;
}>;
const ReadonlyItem = z.toZod<ReadonlyItem>()(z.object({ id: z.string() }).readonly());
expectTypeOf<z.output<typeof ReadonlyItem>>().toEqualTypeOf<ReadonlyItem>();
const Transformed = z.toZod<{ count: number }>()(
z.object({
count: z.string().transform((value) => value.length),
})
);
expectTypeOf<z.input<typeof Transformed>>().toEqualTypeOf<{ count: string }>();
expectTypeOf<z.output<typeof Transformed>>().toEqualTypeOf<{ count: number }>();
// An enum target matches z.enum(...) even though the enum reference is not identical to the union of its members (#6524)
enum SkillLevel {
Noob = "noob",
Pro = "pro",
}
enum Rank {
First = 1,
Second = 2,
}
z.toZod<SkillLevel>()(z.enum(SkillLevel));
const Enums = z.toZod<{
skill: SkillLevel;
members: SkillLevel.Noob | SkillLevel.Pro;
rank: Rank;
maybe: SkillLevel | null;
list: SkillLevel[];
nested: { skill: SkillLevel };
}>()(
z.object({
skill: z.enum(SkillLevel),
members: z.enum(SkillLevel),
rank: z.enum(Rank),
maybe: z.enum(SkillLevel).nullable(),
list: z.array(z.enum(SkillLevel)),
nested: z.object({ skill: z.enum(SkillLevel) }),
})
);
expectTypeOf<z.output<typeof Enums>["skill"]>().toEqualTypeOf<SkillLevel>();
// @ts-expect-error a member subset is not the enum
z.toZod<{ skill: SkillLevel }>()(z.object({ skill: z.literal(SkillLevel.Noob) }));
// @ts-expect-error plain literals do not match a nominal enum target
z.toZod<{ skill: SkillLevel }>()(z.object({ skill: z.enum(["noob", "pro"]) }));
// @ts-expect-error an enum schema does not match a plain literal-union target
z.toZod<{ skill: "noob" | "pro" }>()(z.object({ skill: z.enum(SkillLevel) }));
// @ts-expect-error plain numeric literals do not match a numeric enum target either
z.toZod<{ rank: Rank }>()(z.object({ rank: z.union([z.literal(1), z.literal(2)]) }));
z.toZod<Company>()(
// @ts-expect-error missing optional keys still fail exact output matching
z.object({
id: z.string(),
name: z.string(),
})
);
z.toZod<Company>()(
// @ts-expect-error extra keys fail exact output matching
z.object({
id: z.string(),
name: z.string(),
webAddress: z.string().url().nullable(),
extra: z.string(),
})
);
z.toZod<Company>()(
// @ts-expect-error wrong property output fails matching
z.object({
id: z.number(),
name: z.string(),
webAddress: z.string().url().nullable(),
})
);
z.toZod<ReadonlyItem>()(
// @ts-expect-error readonly output is required
z.object({
id: z.string(),
})
);
// @ts-expect-error any is only an exact match for any
z.toZod<string>()(z.any());
// @ts-expect-error toZod checks output, not input
z.toZod<string>()(z.string().transform((value) => value.length));
// Exact equality is deliberately the only mode. A bidirectional-assignability mode would accept both of the two cases above — `any` is assignable in both directions by construction, and `readonly` is not part of assignability — so it would silently drop the guarantees the rest of this test pins.
// An intersection and the flat object with the same keys are interchangeable as targets, since the normalizer flattens both.
type Intersected = { a: number } & { b: string };
const viaAnd = z.object({ a: z.number() }).and(z.object({ b: z.string() }));
const viaExtend = z.object({ a: z.number() }).safeExtend({ b: z.string() });
z.toZod<Intersected>()(viaAnd);
z.toZod<Intersected>()(viaExtend);
z.toZod<{ a: number; b: string }>()(viaAnd);
z.toZod<{ a: number; b: string }>()(viaExtend);
// @ts-expect-error a missing key is still a missing key
z.toZod<Intersected>()(z.object({ a: z.number() }));
// `keyof` a function is `never`, so the normalizer must not walk into one — mapping it would erase the signature and make every callable compare equal
z.toZod<() => string>()(z.custom<() => string>());
// @ts-expect-error an incompatible callable output is still rejected
z.toZod<() => string>()(z.custom<() => number>());
// @ts-expect-error and so is an incompatible function-valued property
z.toZod<{ cb: (x: number) => void }>()(z.object({ cb: z.custom<(x: string) => void>() }));
// @ts-expect-error a method-bearing generic keeps its type arguments
z.toZod<{ m: Map<string, number> }>()(z.object({ m: z.custom<Map<string, string>>() }));
});
test("checks", () => {
const _a: z.core.$ZodCheck = {} as any as z.core.$ZodChecks;
const _b: z.core.$ZodCheck = {} as any as z.core.$ZodStringFormatChecks;
const _c: z.core.$ZodType = {} as any as z.core.$ZodTypes;
const _d: z.core.$ZodType = {} as any as z.core.$ZodStringFormatTypes;
z.currencyCode() satisfies z.core.$ZodStringFormatTypes;
});
test("assignability to $ZodType", () => {
z.string() satisfies z.ZodType;
z.number() satisfies z.ZodType;
z.boolean() satisfies z.ZodType;
z.object({ key: z.string() }) satisfies z.ZodType;
z.object({ key: z.string() }) satisfies z.ZodType<{ key: string }>;
z.array(z.string()) satisfies z.ZodType;
z.union([z.string(), z.number()]) satisfies z.ZodType;
z.intersection(z.string(), z.number()) satisfies z.ZodType;
z.tuple([z.string(), z.number()]) satisfies z.ZodType;
z.record(z.string(), z.number()) satisfies z.ZodType;
z.map(z.string(), z.number()) satisfies z.ZodType;
z.set(z.string()) satisfies z.ZodType;
z.literal("example") satisfies z.ZodType;
expectTypeOf<z.ZodType extends z.core.$ZodType ? true : false>().toEqualTypeOf<true>();
});
test("assignability with narrowing", () => {
type _RefinedSchema<T extends z.ZodType<object> | z.ZodUnion> = T extends z.ZodUnion
? RefinedUnionSchema<T> // <-- Type instantiation is excessively deep and possibly infinite.
: T extends z.ZodType<object>
? RefinedTypeSchema<z.output<T>> // <-- Type instantiation is excessively deep and possibly infinite.
: never;
type RefinedTypeSchema<T extends object> = T;
type RefinedUnionSchema<T extends z.ZodUnion> = T;
});
test("generic assignability in objects", () => {
interface SortItem<T extends string> {
key: T;
order: string;
}
const createSortItemSchema = <T extends z.ZodType<string>>(sortKeySchema: T) =>
z.object({
key: sortKeySchema,
order: z.string(),
});
<T extends z.ZodType<string>>(sortKeySchema: T, defaultSortBy: SortItem<z.output<T>>[] = []) =>
createSortItemSchema(sortKeySchema).array().default(defaultSortBy);
});