Check if one Zod schema can be safely used where another Zod schema is expected. Think of it as a runtime analogue of TypeScript’s "A extends B" (assignability) relation, applied to Zod v4 schemas.
In TypeScript, a type A is assignable to type B if every value that fits A is also valid for B. Common mental models:
- Literals and enums: narrower sets extend broader sets (
'a' -> string,'a' | 'b' -> 'a' | 'b' | 'c'). - Objects: a source must include at least the properties the target requires, with compatible property types; extra source properties are fine.
- Arrays: element types are covariant (
string[] -> (string | number)[]). - Tuples: same length, and each position must be assignable.
- Unions: a source union extends a target only if every option in the source can be assigned to the target.
- Intersections: a source intersection extends a target only if both sides do.
- Special types:
neverextends everything; everything extendsunknownandany(conservatively).
This library brings those rules to Zod schemas at runtime.
pnpm add zod-assignability zod- Requires
zod@^4.
If you’re working locally in this repo, you can import from the source:
import { isAssignable } from './src/assignability';Otherwise, after publishing/installing:
import { isAssignable } from 'zod-assignability';isAssignable(source: SomeType, target: SomeType): boolean- Returns
trueifsourceis assignable totargetunder the conservative rules described below. SomeTyperefers to Zod v4 core types (import type { SomeType } from 'zod/v4/core').
- Returns
import { z } from 'zod';
import { isAssignable } from 'zod-assignability';
// Primitives
isAssignable(z.string(), z.string()); // true
isAssignable(z.string(), z.number()); // false
// Literals and unions
isAssignable(z.literal('a'), z.string()); // true
isAssignable(
z.union([z.literal('a'), z.literal('b')]),
z.union([z.literal('a'), z.literal('b'), z.literal('c')]),
); // true
// Objects
const A = z.object({ name: z.string() });
const B = z.object({ name: z.string(), age: z.number() });
isAssignable(A, B); // false (B requires age)
isAssignable(B, A); // true (extra props are okay)
// Optional target property
const C = z.object({ name: z.string(), age: z.number().optional() });
isAssignable(B, C); // true
isAssignable(A, C); // true (A is assignable because C's 'age' is optional)-
Unknown, Any, Never
A -> unknown: alwaystrue.A -> any: treated astrue.any -> B: treated astrue.never -> B: alwaystrue.unknown -> B(whenBis notunknown):false.
-
Optional / Nullable wrappers
- Target optional/nullable: matching the inner type or the wrapper value is allowed.
isAssignable(z.string(), z.string().optional()); // true isAssignable(z.null(), z.string().nullable()); // true
- Source optional/nullable: behaves like unions for variance.
isAssignable(z.string().optional(), z.string()); // false (needs `string` and `undefined` -> `string`) isAssignable(z.string().nullable(), z.union([z.string(), z.null()])); // true
- Target optional/nullable: matching the inner type or the wrapper value is allowed.
-
Literals and Enums
- Literal to literal: values must match.
- Literal to primitive: allowed if the literal’s value is of that primitive.
- Literal to union: literal must match at least one union member.
- Enum to enum: source values must be a subset of target values.
- Enum to primitive: allowed when every enum member is of that primitive.
const E1 = z.enum(['A', 'B']); const E2 = z.enum(['A', 'B', 'C']); isAssignable(E1, E2); // true isAssignable(E1, z.string()); // true
-
Primitives
- Primitive to primitive: assignable only when types are identical.
isAssignable(z.boolean(), z.boolean()); // true isAssignable(z.boolean(), z.string()); // false
- Primitive to primitive: assignable only when types are identical.
-
Arrays
- Element types are covariant.
isAssignable( z.array(z.string()), z.array(z.union([z.string(), z.number()])), ); // true isAssignable(z.array(z.number()), z.array(z.string())); // false
- Element types are covariant.
-
Tuples
- Invariant length; element-wise assignability.
isAssignable( z.tuple([z.string(), z.number()]), z.tuple([z.string(), z.number()]), ); // true isAssignable(z.tuple([z.string(), z.number()]), z.tuple([z.string()])); // false
- Invariant length; element-wise assignability.
-
Objects (structural)
- Every required key in target must exist in source.
- If target requires a key, source must also require it.
- Property types are covariant; extra properties in source are allowed.
const S = z.object({ name: z.string(), age: z.number() }); const T = z.object({ name: z.string() }); isAssignable(S, T); // true isAssignable(T, S); // false
-
Records
- Key types and value types must be assignable.
const R1 = z.record(z.string(), z.string()); const R2 = z.record(z.string(), z.union([z.string(), z.number()])); isAssignable(R1, R2); // true
- Key types and value types must be assignable.
-
Unions
- Source union to target: every source option must be assignable to target.
- Target union: source is assignable if it’s assignable to at least one member of the target.
const Uab = z.union([z.literal('a'), z.literal('b')]); const Uabc = z.union([z.literal('a'), z.literal('b'), z.literal('c')]); isAssignable(Uab, Uabc); // true isAssignable(Uabc, Uab); // false
-
Intersections
- Source intersection: assignable if the combined source (for objects) or at least one of its components is assignable to the target.
- Target intersection: source must be assignable to both sides.
-
Custom / Instanceof
- Only identical custom schemas are considered assignable (conservative).
const Dog = z.instanceof(class Dog {}); const Animal = z.instanceof(class Animal {}); isAssignable(Dog, Dog); // true isAssignable(Dog, Animal); // false
- Only identical custom schemas are considered assignable (conservative).
- This library inspects Zod v4 core internals (via
schema._zod.def). If Zod’s internals change, helpers may need updates. - Optional property detection in objects also treats
Union<..., undefined>as optional. - Nullable detection focuses on the wrapper form (
z.string().nullable()); unions withnullare not unwrapped as a nullable wrapper. - Excess property checks are not enforced; extra source properties are allowed.
- Instanceof/custom schemas are treated conservatively; only identical schemas are assignable.
- Build:
pnpm build - Test:
pnpm test