Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Canceled after 0s
403 lines
13 KiB
Plaintext
403 lines
13 KiB
Plaintext
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);
|
|
});
|